使用ONNX部署深度學習和傳統機器學習模型

目錄

ONNX簡介

開放神經網絡交換ONNX(Open Neural Network Exchange)是一套表示深度神經網絡模型的開放格式,由微軟和Facebook于2017推出,然后迅速得到了各大廠商和框架的支持。通過短短幾年的發展,已經成為表示深度學習模型的實際標準,并且通過ONNX-ML,可以支持傳統非神經網絡機器學習模型,大有一統整個AI模型交換標準。

ONNX定義了一組與環境和平臺無關的標準格式,為AI模型的互操作性提供了基礎,使AI模型可以在不同框架和環境下交互使用。硬件和軟件廠商可以基于ONNX標準優化模型性能,讓所有兼容ONNX標準的框架受益。目前,ONNX主要關注在模型預測方面(inferring),使用不同框架訓練的模型,轉化為ONNX格式后,可以很容易的部署在兼容ONNX的運行環境中。

ONNX標準介紹

ONNX規范由以下幾個部分組成:

  • 一個可擴展的計算圖模型:定義了通用的計算圖中間表示法(Intermediate Representation)。
  • 內置操作符集:ai.onnxai.onnx.mlai.onnx是默認的操作符集,主要針對神經網絡模型,ai.onnx.ml主要適用于傳統非神經網絡機器學習模型。
  • 標準數據類型。包括張量(tensors)、序列(sequences)和映射(maps)。

目前,ONNX規范有兩個官方變體,主要區別在與支持的類型和默認的操作符集。ONNX神經網絡變體只使用張量作為輸入和輸出;而作為支持傳統機器學習模型的ONNX-ML,還可以識別序列和映射,ONNX-ML為支持非神經網絡算法擴展了ONNX操作符集。

ONNX使用protobuf序列化AI模型,頂層是一個模型(Model)結構,主要由關聯的元數據和一個圖(Graph)組成;圖由元數據、模型參數、輸入輸出、和計算節點(Node)序列組成,這些節點構成了一個計算無環圖,每一個計算節點代表了一次操作符的調用,主要由節點名稱、操作符、輸入列表、輸出列表和屬性列表組成,屬性列表主要記錄了一些運行時常量,比如模型訓練時生成的系數值。

為了更直觀的了解ONNX格式內容,下面,我們訓練一個簡單的LogisticRegression模型,然后導出ONNX。仍然使用常用的分類數據集iris

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y)

clr = LogisticRegression()
clr.fit(X_train, y_train)

使用skl2onnx把Scikit-learn模型序列化為ONNX格式:

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

initial_type = [('float_input', FloatTensorType([1, 4]))]
onx = convert_sklearn(clr, initial_types=initial_type)
with open("logreg_iris.onnx", "wb") as f:
    f.write(onx.SerializeToString())

使用ONNX Python API查看和驗證模型:

import onnx

model = onnx.load('logreg_iris.onnx')
print(model)

輸出模型信息如下:

ir_version: 5
producer_name: "skl2onnx"
producer_version: "1.5.1"
domain: "ai.onnx"
model_version: 0
doc_string: ""
graph {
  node {
    input: "float_input"
    output: "label"
    output: "probability_tensor"
    name: "LinearClassifier"
    op_type: "LinearClassifier"
    attribute {
      name: "classlabels_ints"
      ints: 0
      ints: 1
      ints: 2
      type: INTS
    }
    attribute {
      name: "coefficients"
      floats: 0.375753253698349
      floats: 1.3907358646392822
      floats: -2.127762794494629
      floats: -0.9207873344421387
      floats: 0.47902926802635193
      floats: -1.5524250268936157
      floats: 0.46959221363067627
      floats: -1.2708674669265747
      floats: -1.5656673908233643
      floats: -1.256540060043335
      floats: 2.18996000289917
      floats: 2.2694246768951416
      type: FLOATS
    }
    attribute {
      name: "intercepts"
      floats: 0.24828049540519714
      floats: 0.8415762782096863
      floats: -1.0461325645446777
      type: FLOATS
    }
    attribute {
      name: "multi_class"
      i: 1
      type: INT
    }
    attribute {
      name: "post_transform"
      s: "LOGISTIC"
      type: STRING
    }
    domain: "ai.onnx.ml"
  }
  node {
    input: "probability_tensor"
    output: "probabilities"
    name: "Normalizer"
    op_type: "Normalizer"
    attribute {
      name: "norm"
      s: "L1"
      type: STRING
    }
    domain: "ai.onnx.ml"
  }
  node {
    input: "label"
    output: "output_label"
    name: "Cast"
    op_type: "Cast"
    attribute {
      name: "to"
      i: 7
      type: INT
    }
    domain: ""
  }
  node {
    input: "probabilities"
    output: "output_probability"
    name: "ZipMap"
    op_type: "ZipMap"
    attribute {
      name: "classlabels_int64s"
      ints: 0
      ints: 1
      ints: 2
      type: INTS
    }
    domain: "ai.onnx.ml"
  }
  name: "deedadd605a34d41ac95746c4feeec1f"
  input {
    name: "float_input"
    type {
      tensor_type {
        elem_type: 1
        shape {
          dim {
            dim_value: 1
          }
          dim {
            dim_value: 4
          }
        }
      }
    }
  }
  output {
    name: "output_label"
    type {
      tensor_type {
        elem_type: 7
        shape {
          dim {
            dim_value: 1
          }
        }
      }
    }
  }
  output {
    name: "output_probability"
    type {
      sequence_type {
        elem_type {
          map_type {
            key_type: 7
            value_type {
              tensor_type {
                elem_type: 1
              }
            }
          }
        }
      }
    }
  }
}
opset_import {
  domain: ""
  version: 9
}
opset_import {
  domain: "ai.onnx.ml"
  version: 1
}

我們可以看到頂層字段記錄了一些模型的元數據信息,代表的含義都比較直觀,字段詳細解釋可以參考文檔 Open Neural Network Exchange - ONNXopset_import記錄了該模型引入的操作符集。空的domain操作符集表示引入ONNX默認的操作符集ai.onnx。ai.onnx.ml代表支持傳統非神經網絡模型操作符集,比如以上模型中的LinearClassifier、NormalizerZipMap。圖(graph)中定義了以下元素:

  • 四個計算節點(node)。
  • 一個輸入變量float_input,類型為1*4的張量,elem_type是一個DataType枚舉型變量,1代表FLOAT。
  • 兩個輸出變量output_labeloutput_probability,output_label類型為維數為1的INT64(elem_type: 7)張量,代表預測目標分類; output_probability類型是映射的序列,映射的鍵是INT64(key_type: 7),值為維數為1的FLOAT,代表每一個目標分類的概率。

可以使用Netron,圖像化顯示ONNX模型的計算拓撲圖,以上模型如下圖:

ONNX-graph

下面我們使用ONNX Runtime Python API預測該ONNX模型,當前僅使用了測試數據集中的第一條數據:

import onnxruntime as rt
import numpy
sess = rt.InferenceSession("logreg_iris.onnx")
input_name = sess.get_inputs()[0].name
label_name = sess.get_outputs()[0].name
probability_name = sess.get_outputs()[1].name
pred_onx = sess.run([label_name, probability_name], {input_name: X_test[0].astype(numpy.float32)})

# print info
print('input_name: ' + input_name)
print('label_name: ' + label_name)
print('probability_name: ' + probability_name)
print(X_test[0])
print(pred_onx)

打印的模型信息和預測值如下:

input_name: float_input
label_name: output_label
probability_name: output_probability
[5.5 2.6 4.4 1.2]
[array([1], dtype=int64), [{0: 0.012208569794893265, 1: 0.5704444646835327, 2: 0.4173469841480255}]]

完整的程序,可以參考以下notebook:onnx.ipynb

ONNX與PMML

ONNX和PMML都是與平臺和環境無關的模型表示標準,可以讓模型部署脫離模型訓練環境,簡化了部署流程,加速模型快速上線到生產環境中。這兩個標準都得到了各大廠商和框架的支持,具有廣泛的應用。

  • PMML是一個比較成熟的標準,在ONNX誕生之前,可以說是模型表示的實際標準,對傳統數據挖掘模型有豐富的支持,最新 PMML4.4 可以支持多達19種模型類型。但是,目前PMML缺乏對深度學習模型的支持,下一版本5.0有可能會添加對深度神經網絡的支持,但是因為PMML是基于老式的XML格式,使用文本格式來存儲深度神經網絡模型結構和參數會帶來模型大小和性能的問題,目前該問題還沒有一個完美的解決方案。關于PMML的詳細介紹,可以參考文章《使用PMML部署機器學習模型》。

  • ONNX作為一個新的標準,剛開始主要提供對深度神經網絡模型的支持,解決模型在不同框架下互操作和交換的問題。目前通過ONNX-ML,ONNX已經可以支持傳統非神經網絡機器學習模型,但是目前模型類型還不夠豐富。ONNX使用protobuf二進制格式來序列化模型,可以提供更好的傳輸性能。

ONNX和PMML這兩種格式都有成熟的開源類庫和框架支持,PMML有JPMML,PMML4S,PyPMML等。ONNX有微軟的ONNX runtime,NVIDIA TensorRT等。用戶可以根據自己的實際情況選擇合適的跨平臺格式來部署AI模型。

DaaS簡介

DaaS(Deployment-as-a-Service)是AutoDeployAI公司推出的AI模型自動部署系統,支持多種模型類型的上線部署,以下我們介紹如何在DaaS中使用ONNX格式來部署傳統機器學習模型和深度神經網絡學習模型,DaaS使用ONNX Runtime作為ONNX模型的執行引擎,ONNX Runtime是微軟開源的ONNX預測類庫,提供高性能預測服務功能。首先,登陸DaaS系統后,創建一個新的工程ONNX,下面的操作都在該工程下進行。關于DaaS的詳細信息,可以參考文章《自動部署PMML模型生成REST API》。

使用ONNX部署傳統機器學習模型

  1. 導入模型。選擇上面訓練的Logistic Regression模型logreg_iris.onnx

    daas-import-model-logreg

    導入成功后,頁面轉到模型主頁面。可以看到模型有一個輸入字段float_input,類型是tensor(float),維數(1,4)。兩個輸出字段:output_labeloutput_probability。

    daas-model-overview-logreg
  2. 測試模型。點擊標簽頁測試,輸入預測數據[[5.5, 2.6, 4.4, 1.2]],然后點擊提交命令,輸出頁面顯示預測測試結果:

    daas-model-test-logreg
  3. 創建默認實時預測Web服務。點擊標簽頁部署,然后點擊添加服務命令,輸入服務名稱,其他使用默認值:

    daas-create-web-service-logreg
  4. 測試Web服務。服務創建成功后,頁面轉到服務部署主頁,當服務副本狀態為運行中時,代表Web服務已經成功上線,可以接受外部請求。有兩種方式測試該服務:

    • 在DaaS系統中通過測試頁面。點擊標簽頁測試,輸入JSON格式的請求正文,點擊提交命令:

      daas-test-web-service-logreg
    • 通過任意的RSET客戶端,使用標準的REST API來測試。這里我們使用curl命令行程序來調用Web服務,點擊生成代碼命令,彈出顯示使用curl命令調用REST API的對話框:

      daas-curl-logreg

      復制該curl命令,打開shell頁面,執行命令:

      daas-run-curl-logreg

使用ONNX部署深度神經網絡模型

我們嘗試部署ONNX Model Zoo中已經訓練好的模型,這里我們選擇MNIST-手寫數字識別CNN模型,下載基于ONNX1.3的模型最新版本:mnist.tar.gz。

  1. 導入模型。選擇已下載模型mnist.tar.gz

    daas-import-model-mnist

    導入成功后,頁面轉到模型主頁面??梢钥吹侥P陀幸粋€輸入字段Input3,類型是tensor(float),維數(1,1,28,28)。一個輸出字段:Plus214_Output_0,類型同樣是tensor(float),維數(1,10)。

    daas-model-overview-mnist
  2. 測試模型。點擊標簽頁測試,然后點擊JSON命令,DaaS系統會自動創建符合輸入數據格式的隨機數據,以方便測試。點擊提交命令,輸出頁面顯示預測測試結果:

    daas-model-test-mnist
  3. 創建自定義實施預測腳本。為了能支持輸入圖像,并且直接輸出預測值,我們需要創建自定義預測腳本。點擊標簽頁實時預測,然后點擊生成自定義實時預測腳本命令,

    daas-generate-custom-scoring-mnist

    腳本生成后,點擊命令作為API測試,進入腳本測試頁面,我們可以自由添加自定義預處理和后處理功能。添加以下函數預處理圖像:

    def rgb2gray(rgb):
        """Convert the input image into grayscale"""
        import numpy as np
        return np.dot(rgb[...,:3], [0.299, 0.587, 0.114])
    
    
    def resize_img(img_to_resize):
        """Resize image to MNIST model input dimensions"""
        import cv2
        r_img = cv2.resize(img_to_resize, dsize=(28, 28), interpolation=cv2.INTER_AREA)
        r_img.resize((1, 1, 28, 28))
        return r_img
    
    
    def preprocess_image(img_to_preprocess):
        """Resize input images and convert them to grayscale."""
        if img_to_preprocess.shape == (28, 28):
            img_to_preprocess.resize((1, 1, 28, 28))
            return img_to_preprocess
    
        grayscale = rgb2gray(img_to_preprocess)
        processed_img = resize_img(grayscale)
        return processed_img
    

    在已有preprocess_files函數中調用preprocess_image,代碼如下:

    import matplotlib.image as mpimg
    for key, file in files.items():
        img = mpimg.imread(file)
        record[key] = preprocess_image(img)
    

    在已有postprocess函數中添加如下代碼后處理預測結果以獲取最終的預測值:

    def postprocess(result):
        """postprocess the predicted results"""
        import numpy as np
        return [int(np.argmax(np.array(result).squeeze(), axis=0))]
    

    點擊命令保存,然后在請求頁面中輸入函數名為predict,選擇請求正文基于表單,輸入表單名稱為模型唯一的輸入字段名Input3,類型選擇文件,點擊上傳,選擇測試圖像2.png,最后點擊提交命令,測試該腳本是否按照我們的期望工作:

    daas-test-custom-scoring-mnist
  4. 創建正式部署Web服務。當腳本測試成功后,點擊部署標簽頁,然后點擊添加網絡服務命令,輸入服務名稱,其他使用默認值:

    daas-create-web-service-mnist
  5. 測試Web服務。服務創建成功后,頁面轉到服務部署主頁,當服務副本狀態為運行中時,代表Web服務已經成功上線,可以接受外部請求。有兩種方式測試該服務:

    • 在DaaS系統中通過測試頁面。點擊標簽頁測試,選擇請求正文基于表單,選擇輸入測試圖像5.jpg,點擊提交命令:

      daas-test-web-service-mnist.png
    • 通過任意的RSET客戶端,使用標準的REST API來測試。這里我們使用curl命令行程序來調用Web服務,點擊生成代碼命令,彈出顯示使用curl命令調用REST API的對話框:

      daas-curl-mnist

      復制該curl命令,打開shell頁面,切換到圖像目錄下,執行命令:

      daas-run-curl-mnist

總結

本文中我們介紹了ONNX這種跨平臺AI模型表示標準,以及與PMML的區別,最后演示了如何在DaaS系統中通過ONNX部署傳統機器學習模型和深度神經網絡模型,可以看到ONNX讓模型部署脫離了模型訓練環境,極大簡化了整個部署流程。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379