如何在Python中把機器學習模型轉成API
作者: ofollow,noindex">Sayak Paul
編譯:Bot
設想這麼一種情況:
你構建了一個非常好的機器學習模型,比方說它可以預測某種交易中是否存在欺詐嫌疑。現在,你的一個朋友正在為某家銀行開發Android APP,他希望能把你的模型整合到他們的產品裡,因為你的模型太實用了,效能也格外出色。
但是,那個Android APP是用JAVA寫的,你的模型是用Python寫的。怎麼辦?難道你還要花時間花精力用JAVA重新寫一個?
這時候,你就需要一種祕密武器——API。在實踐中,上述情況是把機器學習模型轉換成API的常見需求之一,這一點非常重要,因為現在各行各業都在尋找可以把技術用於生產、經營的資料科學家。本文將介紹建立API的具體操作,具體來說,它主要涵蓋以下內容:
什麼是API
簡單來說,一個API其實就是兩個軟體之間的(假定)契約,如果面向終端使用者的軟體能以預定義的格式提供輸入,另一個軟體就能擴充套件其功能,並向面向終端使用者的軟體提供輸出結果。——Analytics Vidhya
從本質上講,API非常類似Web應用程式,但前者往往以標準資料交換格式返回資料(如JSON、XML等)。一旦開發人員拿到了所需的輸出,他們就能按照各種需求對它進行設計。現在有很多流行的機器學習API,比如IBM Watson就有以下功能:
- 機器翻譯:將一種語言的文字輸入翻譯為終端使用者的目標語言,支援英語、葡萄牙語、西班牙語和法語。
- Message Resonance:分析草稿內容,並對它被一個特定的目標受眾接受的可能性進行評分。
- Q&A:直接根據選定和收集到資料正文或“語料庫”中的主要資料來源,解釋和回答使用者問題。
- User Modeling:使用語言分析從一個人的通訊方式中提取一組個性和社會特徵。
Google Vision API也是一個很好的例子,它主要面向計算機視覺任務。
基本上,大多數雲服務提供商都會提供一系列大型、綜合性的API,而以小規模機器學習為重點的企業則提供即用型API。它們都滿足了那些沒有太多機器學習專業知識背景的開發人員/企業的需求,方便他們在流程和產品套件中部署機器學習技術。
在Web開發中,一些比較流行的機器學習API有DialogFlow、Microsoft的Cognitive Toolkit、TensorFlow.js等。
Flask基礎入門
要入門Flask,首先我們得知道什麼是Web服務。Web服務是API的一種形式,它假定API通過伺服器託管,並且可以被呼叫。Web API/Web Service——這些術語通常可以互換使用。
Flask是一個用Python編寫的輕量級Web服務框架,當然,它不是Python中的唯一框架,同類競品還有Django、Falcon、Hug等。但本文只介紹如何用Flask建立API。
如果你下載了Anaconda版,裡面就已經包含了Flask。如果你想用pip:
pip install flask
你會發現它非常小,這也是它深受Python開發人員喜愛的一個原因。而另一個原因就是Flask框架附帶內建的輕量級Web伺服器,需要的配置少,而且可以用Python程式碼直接控制。
下面的程式碼很好地展示了Flask的簡約性。它建立一個簡單的Web-API,在接收到特定URL時會生成一個特定的輸出。
from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Welcome to machine learning model APIs!" if __name__ == '__main__': app.run(debug=True)
執行後,你可以在終端瀏覽器中輸入這個網址,然後觀察結果。

一些要點
- Jupyter Notebook非常適合處理有關Python、R和markdown的東西。但一旦涉及構建web伺服器,它就會出現很多奇怪的bug。所以建議大家最好在Sublime等文字編輯器裡編寫Flask程式碼,並從終端/命令提示符執行程式碼。
- 千萬不要把檔案命名為flask.py。
- 預設情況下,執行Flask的埠號是5000。有時伺服器能在這個埠上正常啟動,但有時,如果你是在Web瀏覽器或任何API客戶端(如Postman)中用URL啟動,它可能會報錯,比如下圖:

app.run(debug=True,port=12345)

現在我們來看看輸入的程式碼:
- 建立Flask例項後,Python會自動生成一個name變數。如果這個檔案是作為指令碼直接用Python執行的,那麼這個變數將為“main”;如果是匯入檔案,那麼“name” 的值將是你匯入檔案的名稱。例如,如果你有
test.py
和run.py
,並且將test.py
匯入run.py
,那麼test.py
的“name”值就會是test(app = Flask(test))
。 - 關於上面
hello()
的定義,可以用 @app .route(“/“)。同時,裝飾器route()
可以告訴Flask什麼URL可以觸發定義好的hello()
。 -
hello()
的作用是在使用API時生成輸出。在這種情況下,在Web瀏覽器轉到localhost:5000/
會產生預期的輸出(假設是預設埠)。
如果我們想為機器學習模型建立API,下面是一些需要牢記的東西。
構建機器學習模型
在這裡,我們以最常規的Scikit-learn模型為例,介紹一下怎麼用Flask學習Scikit-learn模型。首先,我們來回顧一下Scikit-learn的常用模組:
- 聚類
- 迴歸
- 分類
- 降維
- 模型選擇
- 預處理
對於一般資料,我們在進行傳送和接收時會涉及將物件轉化為便於傳輸的格式的操作,它們也被稱為物件的序列化(serialization)和反序列化(deserialization)。模型和資料很不一樣,但Scikit-learn剛好支援對訓練模型的序列化和反序列化,這就為我們節省了重新訓練模型的時間。通過使用scikit-learn中的模型序列化副本,我們可以編寫Flask API。
同時,Scikit-learn模型的一個要求是資料必需採用數字格式,這就是為什麼我們需要把資料集裡的分類特徵轉成數字特徵0和1。事實上,除了分類,Scikit-learn的 sklearn.preprocessing
模組還提供諸如 LabelEncoder
、 OneHotEncoder
等編碼方法。
此外,對於資料集裡的缺失值,Scikit-learn不能自動填充,而是需要我們自己手動處理,然後再輸入模型。缺失值和上面提到的特徵編碼其實都是資料預處理的重要步驟,它們對構建效能良好的機器學習模型非常重要。
為了方便演示,這裡我們以Kaggle上最受歡迎的資料集——泰坦尼克為例進行講解。這個資料集主要是個分類問題,我們的任務是根據表格資料預測乘客的生存概率。為了進一步簡化,我們只用四個變數:age(年齡)、sex(性別)、embarked(登船港口:C=Cherbourg, Q=Queenstown, S=Southampton)和survived。其中survived是個類別標籤。
# Import dependencies import pandas as pd import numpy as np # Load the dataset in a dataframe object and include only four features as mentioned url = "http://s3.amazonaws.com/assets.datacamp.com/course/Kaggle/train.csv" df = pd.read_csv(url) include = ['Age', 'Sex', 'Embarked', 'Survived'] # Only four features df_ = df[include]
“Sex”和“Embarked”是非數字的分類特徵,我們需要對它們進行編碼;“age”這個特徵有不少缺失值,這點可以彙總統計後用中位數或平均數來填充;Scikit-learn不能識別NaN,所以我們還要為此編寫一個輔助函式:
categoricals = [] for col, col_type in df_.dtypes.iteritems(): if col_type == 'O': categoricals.append(col) else: df_[col].fillna(0, inplace=True)
上面的程式碼是為資料集填補缺失值。這裡需要注意一點,缺失值對模型效能其實很重要,尤其是當空值過多時,我們用單個值填充要非常謹慎,不然很可能會導致很大的偏差。在這個資料集裡,因為有缺失值的列是age,所以我們不應該用0填充NaN。
至於把非數字特徵轉成數字行駛,你可以用One Hot Encoding,也可以用Pandas提供的 get_dummies()
:
df_ohe = pd.get_dummies(df_, columns=categoricals, dummy_na=True)
現在我們已經完成了預處理,可以準備訓練機器學習模型了:選擇Logistic迴歸分類器。
from sklearn.linear_model import LogisticRegression dependent_variable = 'Survived' x = df_ohe[df_ohe.columns.difference([dependent_variable])] y = df_ohe[dependent_variable] lr = LogisticRegression() lr.fit(x, y) LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1, penalty='l2', random_state=None, solver='liblinear', tol=0.0001, verbose=0, warm_start=False)
有了模型,之後就是儲存模型。從技術上講這裡我們應該對模型做序列化,在Python裡,這個操作被稱為Pickling。
儲存機器學習模型:序列化和反序列化
呼叫sklearn的 joblib
:
from sklearn.externals import joblib joblib.dump(lr, 'model.pkl') ['model.pkl']
Logistic迴歸模型現在保持不變,我們可以用一行程式碼把它載入到記憶體中,而把模型載入回工作區的操作就是反序列化。
lr = joblib.load('model.pkl')
用Flask為模型建立API
要用Flask為模型建立伺服器,我們要做兩件事:
- 當APP啟動時把已經存在的模型載入到記憶體中。
- 建立一個API斷電,它接受輸入變數,將它們轉換為適當的格式,並返回預測。
更具體地說,當你輸入以下內容時:
[ {"Age": 85, "Sex": "male", "Embarked": "S"}, {"Age": 24, "Sex": '"female"', "Embarked": "C"}, {"Age": 3, "Sex": "male", "Embarked": "C"}, {"Age": 21, "Sex": "male", "Embarked": "S"} ]
你希望API的輸出會是:
{"prediction": [0, 1, 1, 0]}
其中0表示遇難,1表示倖存。這裡輸入格式是JSON,它是最廣泛使用的資料交換格式之一。
要做到上述效果,我們需要先編寫一個函式 predict()
,它的目標如前所述:
- 當APP啟動時把已經存在的模型載入到記憶體中。
- 建立一個API斷電,它接受輸入變數,將它們轉換為適當的格式,並返回預測。
我們已經演示瞭如何載入已有模型,之後是根據接收的輸入預測人員生存狀態:
from flask import Flask, jsonify app = Flask(__name__) @app.route('/predict', methods=['POST']) def predict(): json_ = request.json query_df = pd.DataFrame(json_) query = pd.get_dummies(query_df) prediction = lr.predict(query) return jsonify({'prediction': list(prediction)})
雖然看起來挺簡單,但你可能會在這個步驟遇到一個小問題。
為了讓你編寫的函式能正常執行,傳入請求中必需包含這四個分類變數的所有可能值,這些值可能是實時的,也可能不是。如果傳入請求裡出現必要值缺失,那麼根據當前方法定義的 predict()
生成的資料列會比分類器裡少,模型就會報錯。
要解決這個問題,我們需要在模型訓練期間把列保留下來,把任何Python物件序列化為.pkl檔案。
model_columns = list(x.columns) joblib.dump(model_columns, 'model_columns.pkl') ['model_columns.pkl']
由於已經保留了列列表,所以你可以在預測時處理缺失值(記得在APP啟動前載入模型):
@app.route('/predict', methods=['POST']) # Your API endpoint URL would consist /predict def predict(): if lr: try: json_ = request.json query = pd.get_dummies(pd.DataFrame(json_)) query = query.reindex(columns=model_columns, fill_value=0) prediction = list(lr.predict(query)) return jsonify({'prediction': prediction}) except: return jsonify({'trace': traceback.format_exc()}) else: print ('Train the model first') return ('No model here to use')
你已經在“/ predict”API中包含了所有必需元素,現在你只需要編寫主類:
if __name__ == '__main__': try: port = int(sys.argv[1]) # This is for a command-line argument except: port = 12345 # If you don't provide any port then the port will be set to 12345 lr = joblib.load(model_file_name) # Load "model.pkl" print ('Model loaded') model_columns = joblib.load(model_columns_file_name) # Load "model_columns.pkl" print ('Model columns loaded') app.run(port=port, debug=True)
現在,這個API就全部完成可以託管了。
當然,如果你想把Logistic迴歸模型程式碼和Flask API程式碼分離為單獨的.py檔案,這其實是一種很好的程式設計習慣。那麼你的 model.py
程式碼應該如下所示:
# Import dependencies import pandas as pd import numpy as np # Load the dataset in a dataframe object and include only four features as mentioned url = "http://s3.amazonaws.com/assets.datacamp.com/course/Kaggle/train.csv" df = pd.read_csv(url) include = ['Age', 'Sex', 'Embarked', 'Survived'] # Only four features df_ = df[include] # Data Preprocessing categoricals = [] for col, col_type in df_.dtypes.iteritems(): if col_type == 'O': categoricals.append(col) else: df_[col].fillna(0, inplace=True) df_ohe = pd.get_dummies(df_, columns=categoricals, dummy_na=True) # Logistic Regression classifier from sklearn.linear_model import LogisticRegression dependent_variable = 'Survived' x = df_ohe[df_ohe.columns.difference([dependent_variable])] y = df_ohe[dependent_variable] lr = LogisticRegression() lr.fit(x, y) # Save your model from sklearn.externals import joblib joblib.dump(lr, 'model.pkl') print("Model dumped!") # Load the model that you just saved lr = joblib.load('model.pkl') # Saving the data columns from training model_columns = list(x.columns) joblib.dump(model_columns, 'model_columns.pkl') print("Models columns dumped!")
而 api.py
則是:
# Dependencies from flask import Flask, request, jsonify from sklearn.externals import joblib import traceback import pandas as pd import numpy as np # Your API definition app = Flask(__name__) @app.route('/predict', methods=['POST']) def predict(): if lr: try: json_ = request.json print(json_) query = pd.get_dummies(pd.DataFrame(json_)) query = query.reindex(columns=model_columns, fill_value=0) prediction = list(lr.predict(query)) return jsonify({'prediction': str(prediction)}) except: return jsonify({'trace': traceback.format_exc()}) else: print ('Train the model first') return ('No model here to use') if __name__ == '__main__': try: port = int(sys.argv[1]) # This is for a command-line input except: port = 12345 # If you don't provide any port the port will be set to 12345 lr = joblib.load("model.pkl") # Load "model.pkl" print ('Model loaded') model_columns = joblib.load("model_columns.pkl") # Load "model_columns.pkl" print ('Model columns loaded') app.run(port=port, debug=True)
現在,你可以在名為Postman的API客戶端中測試此API 。只要確保model.py與api.py在同一個目錄下,並確保兩者都已在測試前編譯好了,如下圖所示:

如果所有檔案都已成功編譯,目錄結構應該如下圖所示:

注:IPYNB檔案是可選的。
在Postman中測試API
Postman是測試API最好用的工具之一。如果你下載了最新版本,它的介面應該如下所示:

成功啟動Flask伺服器後,你需要在Postman中輸入包含正確埠號的正確URL:

恭喜!你剛剛構建了第一個機器學習API。這是個可以根據泰坦尼克號乘客age、sex和embarked資訊預測他們生存狀態的API,現在,你的朋友就能用前端程式碼呼叫它,輸出神奇的結果。