1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 9462字,需瀏覽 19分鐘

         ·

        2020-08-22 06:52


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


        Author:louwill

        Machine Learning Lab

        ? ? ?

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


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


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


        1. 基于web端的模型部署

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

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

        tensorflow 1.14keras 2.2.4flask 1.1.1redis 3.3.8

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


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

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


        ???? 因為圖像數(shù)據(jù)作為numpy數(shù)組不能直接存儲到Redis中,所以圖像存入到數(shù)據(jù)庫之前需要將其序列化編碼,從數(shù)據(jù)庫取出時再將其反序列化解碼即可。分別定義編碼和解碼函數(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ù)測圖像還需要進行簡單的預(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ù)測接口定義

        ???? 準(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ù)測圖像隊列        queue = db.lrange(IMAGE_QUEUE, 0, BATCH_SIZE - 1)        imageIDs = []        batch = None        # 遍歷隊列        for q in queue:            # 獲取隊列中的圖像并反序列化解碼            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ù)測結(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ù)庫                db.set(imageID, json.dumps(output))            # 從隊列中刪除已預(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")            # 生成圖像ID            k = str(uuid.uuid4())            d = {"id": k, "image": base64_encode_image(image)}            db.rpush(IMAGE_QUEUE, json.dumps(d))            # 運行服務(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)部自動將請求的URL和目標(biāo)函數(shù)關(guān)聯(lián)了起來,這樣方便我們快速搭建一個Web服務(wù)。


        1.4 接口測試

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

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

        模型端的返回:

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


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

        from threading import Threadimport requestsimport time# 請求的URLKERAS_REST_API_URL = "http://127.0.0.1:5000/predict"# 測試圖片IMAGE_PATH = "test.jpg"# 并發(fā)數(shù)NUM_REQUESTS = 500# 請求間隔SLEEP_COUNT = 0.05def call_predict_endpoint(n):    # 上傳圖像    image = open(IMAGE_PATH, "rb").read()    payload = {"image": image}    # 提交請求    r = requests.post(KERAS_REST_API_URL, files=payload).json()    # 確認請求是否成功    if r["success"]:        print("[INFO] thread {} OK".format(n))    else:        print("[INFO] thread {} FAILED".format(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)

        測試效果如下:



        2. 基于C++的模型部署

        2.1 引言

        ???? PyTorch作為一款端到端的深度學(xué)習(xí)框架,在1.0版本之后已具備較好的生產(chǎn)環(huán)境部署條件。除了在web端撰寫REST API進行部署之外(參考),軟件端的部署也有廣泛需求。尤其是最近發(fā)布的1.5版本,提供了更為穩(wěn)定的C++前端API。


        ???? 工業(yè)界與學(xué)術(shù)界最大的區(qū)別在于工業(yè)界的模型需要落地部署,學(xué)界更多的是關(guān)心模型的精度要求,而不太在意模型的部署性能。一般來說,我們用深度學(xué)習(xí)框架訓(xùn)練出一個模型之后,使用Python就足以實現(xiàn)一個簡單的推理演示了。但在生產(chǎn)環(huán)境下,Python的可移植性和速度性能遠不如C++。所以對于深度學(xué)習(xí)算法工程師而言,Python通常用來做idea的快速實現(xiàn)以及模型訓(xùn)練,而用C++作為模型的生產(chǎn)工具。目前PyTorch能夠完美的將二者結(jié)合在一起。實現(xiàn)PyTorch模型部署的核心技術(shù)組件就是TorchScript和libtorch。


        ???? 所以基于PyTorch的深度學(xué)習(xí)算法工程化流程大體如下圖所示:


        2.2 TorchScript

        ???? TorchScript可以視為PyTorch模型的一種中間表示,TorchScript表示的PyTorch模型可以直接在C++中進行讀取。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)建模型實例?my_model?=?MyModel()#?輸入示例x,?h?=?torch.rand(3,?4),?torch.rand(3,?4)#?torch.jit.trace方法對模型構(gòu)建TorchScripttraced_model?=?torch.jit.trace(my_model,?(x,?h))#?保存轉(zhuǎn)換后的模型traced_model.save('model.pt')


        ???? 在這段代碼中,我們先是定義了一個簡單模型并創(chuàng)建模型實例,然后給定輸入示例,Tracing方法最關(guān)鍵的一步在于使用torch.jit.trace方法對模型進行TorchScript轉(zhuǎn)化。我們可以獲得轉(zhuǎn)化后的traced_model對象獲得其計算圖屬性和代碼屬性。計算圖屬性:

        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)


        ???? 這樣我們就可以將整個模型都保存到硬盤上了,并且經(jīng)過這種方式保存下來的模型可以加載到其他其他語言環(huán)境中。


        ???? TorchScript的另一種實現(xiàn)方式是Script的方式,可以算是對Tracing方式的一種補充。當(dāng)模型代碼中含有if或者for-loop等控制流程序時,使用Tracing方式是無效的,這時候可以采用Script方式來進行實現(xiàn)TorchScript。實現(xiàn)方法跟Tracing差異不大,關(guān)鍵在于把jit.tracing換成jit.script方法,示例如下。

        scripted_model?=?torch.jit.script(MyModel)scripted_model.save('model.pt')


        ???? 除了Tracing和Script之外,我們也可以混合使用這兩種方式,這里不做詳述??傊?,TorchScript為我們提供了一種表示形式,可以對代碼進行編譯器優(yōu)化以提供更有效的執(zhí)行。


        2.3 libtorch

        ???? 在Python環(huán)境下對訓(xùn)練好的模型進行轉(zhuǎn)換之后,我們需要C++環(huán)境下的PyTorch來讀取模型并進行編譯部署。這種C++環(huán)境下的PyTorch就是libtorch。因為libtorch通常用來作為PyTorch模型的C++接口,libtorch也稱之為PyTorch的C++前端。


        ???? 我們可以直接從PyTorch官網(wǎng)下載已經(jīng)編譯好的libtorch安裝包,當(dāng)然也可以下載源碼自行進行編譯。這里需要注意的是,安裝的libtorch版本要與Python環(huán)境下的PyTorch版本一致。


        ???? 安裝好libtorch后可簡單測試下是否正常。比如我們用TorchScript轉(zhuǎn)換一個預(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í)行,輸出如下。對比Python環(huá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的描述,其實我們已經(jīng)基本將PyTorch的C++部署已經(jīng)基本講到了,這里我們再來完整的理一下整個流程?;贑++的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";}

        ???? 兩個文件編寫完成之后便可對其執(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é)

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


        往期精彩回顧





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

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

        本站qq群1003271085。

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

        瀏覽 35
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            精品视频亚洲 | 俺也来俺也去色婷婷日韩欧美风 | 国产在线一区二区三区 | 操b免费看| 17c白丝喷水 自慰 | 偷拍自拍第五页 | 操免费视频 | 91偷拍5月偷拍精品 | 国产极品大陆一区二区伦理片 | 91啦丨九色丨国产人 |