在本文中,我們將探討如何為生產(chǎn)準(zhǔn)備機(jī)器學(xué)習(xí)模型,并將其部署在簡(jiǎn)單的 Web 應(yīng)用程序中。部署機(jī)器學(xué)習(xí)模型本身就是一門藝術(shù)。事實(shí)上,將機(jī)器學(xué)習(xí)模型成功投入生產(chǎn)超出了數(shù)據(jù)科學(xué)知識(shí)的范圍,并且涉及許多軟件開(kāi)發(fā)和DevOps技能。你為什么要關(guān)心這一切?目前,數(shù)據(jù)科學(xué)團(tuán)隊(duì)中最有價(jià)值的角色之一是機(jī)器學(xué)習(xí)工程師。這個(gè)角色聚集了兩全其美的精華。
這些工程師不僅知道如何將不同的機(jī)器學(xué)習(xí)和深度學(xué)習(xí)模型應(yīng)用于適當(dāng)?shù)膯?wèn)題,還知道如何測(cè)試它們,驗(yàn)證它們并最終部署它們。擁有一個(gè)能夠?qū)C(jī)器學(xué)習(xí)模型投入生產(chǎn)的人對(duì)任何公司來(lái)說(shuō)都是一筆巨大的資產(chǎn)。一般來(lái)說(shuō),這是Rubik's Code提供的主要服務(wù)類型。為了成為一名成功的機(jī)器學(xué)習(xí)工程師,你需要具備各種技能,而不僅僅是關(guān)注數(shù)據(jù)。實(shí)際上,為機(jī)器學(xué)習(xí)模型編寫(xiě)的代碼量遠(yuǎn)小于支持測(cè)試和提供該模型的代碼量。
MLOps,一個(gè)圍繞這種需求構(gòu)建的工程術(shù)語(yǔ),正在成為一種新的趨勢(shì)。這就是為什么在本文中,我們將重點(diǎn)介紹您需要了解的幾種技術(shù),以便構(gòu)建一個(gè)用戶想要的成功機(jī)器學(xué)習(xí)應(yīng)用程序。本教程中提供的完整解決方案由兩個(gè)組件組成:服務(wù)器端和客戶端。這是基本的 Web 體系結(jié)構(gòu),其中客戶端與應(yīng)用程序的用戶交互并將其發(fā)送到服務(wù)器端。服務(wù)器端執(zhí)行數(shù)據(jù)處理,或者在本例中運(yùn)行預(yù)測(cè),并將結(jié)果返回到客戶端,客戶端將其呈現(xiàn)給用戶。從本質(zhì)上講,在本文中,我們構(gòu)建了一個(gè)可以用下圖表示的小系統(tǒng):
MLOps,一個(gè)圍繞這種需求構(gòu)建的工程術(shù)語(yǔ),正在成為一種新的趨勢(shì)。這就是為什么在本文中,我們將重點(diǎn)介紹您需要了解的幾種技術(shù),以便構(gòu)建一個(gè)用戶想要的成功機(jī)器學(xué)習(xí)應(yīng)用程序。本教程中提供的完整解決方案由兩個(gè)組件組成:服務(wù)器端和客戶端。這是基本的 Web 體系結(jié)構(gòu),其中客戶端與應(yīng)用程序的用戶交互并將其發(fā)送到服務(wù)器端。服務(wù)器端執(zhí)行數(shù)據(jù)處理,或者在本例中運(yùn)行預(yù)測(cè),并將結(jié)果返回到客戶端,客戶端將其呈現(xiàn)給用戶。
為了涵蓋所有內(nèi)容,我們需要涵蓋幾個(gè)主題:
- 安裝和數(shù)據(jù)集
- Rest API 基礎(chǔ)知識(shí)
- FastAPI 基礎(chǔ)知識(shí)
- 客戶端 – 用戶界面
- 服務(wù)器端 – 使用 FastAPI 進(jìn)行模型訓(xùn)練的解決方案
- 服務(wù)器端 – 使用 FastAPI 加載模型的解決方案
1. 安裝和數(shù)據(jù)集
要成功運(yùn)行本教程中的示例,需要安裝 Python 3.6 或更高版本。最簡(jiǎn)單的方法是使用Anaconda分發(fā)。它附帶了本教程所需的所有其他必要庫(kù),如Pandas,NumPy,SciKit Learn等。要安裝 FastAPI 及其所有依賴項(xiàng),請(qǐng)使用以下命令:
pip install fastapi[all]
這包括 Uvicorn,一個(gè)運(yùn)行代碼的 ASGI 服務(wù)器。如果您對(duì)其他一些ASGI服務(wù)器(如Hypercorn)更滿意,那也很好,您可以在本教程中使用它。
對(duì)于Web應(yīng)用程序,我們使用Angular。為此,您需要安裝 Node.js 和 npm。使用Angular框架進(jìn)行操作的最常見(jiàn)方法是使用Angular命令行界面 - Angular-CLI。這個(gè)工具的好處之一是,一旦我們用它初始化了我們的應(yīng)用程序,我們就可以使用TypeScript,它將自動(dòng)轉(zhuǎn)換為JAVAScript。安裝此接口是通過(guò) npm 完成的,當(dāng)然,通過(guò)運(yùn)行以下命令:
npm install -g angular-cli
當(dāng)您要?jiǎng)?chuàng)建新的Angular應(yīng)用程序時(shí),您可以使用以下命令執(zhí)行此操作:
ng new Application_name
此命令將創(chuàng)建一個(gè)文件夾結(jié)構(gòu),供我們的應(yīng)用程序使用。要運(yùn)行此應(yīng)用程序,請(qǐng)將 shell 放在剛剛創(chuàng)建的應(yīng)用程序的根文件夾 (cd application_name) 中,然后調(diào)用命令:
ng serve
如果您在轉(zhuǎn)到瀏覽器并打開(kāi)localhost:4200后按照上述步驟操作,您將能夠看到默認(rèn)的Angular應(yīng)用程序。
我們?cè)诒疚闹惺褂玫臄?shù)據(jù)來(lái)自PalmerPenguins Dataset。該數(shù)據(jù)集最近被引入,作為著名的Iris數(shù)據(jù)集的替代方案。它由Kristen Gorman博士和南極洲LTER的Palmer站創(chuàng)建。您可以在此處或通過(guò)Kaggle獲取此數(shù)據(jù)集。該數(shù)據(jù)集主要由兩個(gè)數(shù)據(jù)集組成,每個(gè)數(shù)據(jù)集包含344只企鵝的數(shù)據(jù)。就像在鳶尾花數(shù)據(jù)集中一樣,有3種不同種類的企鵝來(lái)自帕爾默群島的3個(gè)島嶼。此外,這些數(shù)據(jù)集還包含每個(gè)物種的 culmen 維度。Culmen是鳥(niǎo)喙的上脊。在簡(jiǎn)化的企鵝數(shù)據(jù)中,culmen的長(zhǎng)度和深度被重命名為變量culmen_length_mm和culmen_depth_mm。以下是數(shù)據(jù)集:
2. REST API 基礎(chǔ)知識(shí)
在我們開(kāi)始使用Python和Flask實(shí)現(xiàn)Web應(yīng)用程序之前,讓我們首先找出什么是REST API。現(xiàn)在,我相信你一生中已經(jīng)看過(guò)這個(gè)詞兩次了。術(shù)語(yǔ)的第二部分 - API代表應(yīng)用程序編程接口。從本質(zhì)上講,它 API 表示程序用于相互通信的一組規(guī)則。例如,在服務(wù)器-客戶端體系結(jié)構(gòu)中,應(yīng)用程序的服務(wù)器端以公開(kāi)可由應(yīng)用程序客戶端調(diào)用的方法進(jìn)行編程。這意味著客戶端可以在其代碼中調(diào)用服務(wù)器上的方法,并從中獲取特定結(jié)果。REST 代表"具象狀態(tài)轉(zhuǎn)移"。這表示開(kāi)發(fā)人員在構(gòu)建 API 時(shí)應(yīng)遵循的一組規(guī)則。它定義了 API 的外觀,因此 API 是標(biāo)準(zhǔn)化的。
其中一個(gè)規(guī)則定義了在鏈接特定 URL 時(shí)可以收集數(shù)據(jù)或資源。例如,您可以鏈接"api.rubikscode.com/blogs"并獲取博客列表作為響應(yīng)。URL api.rubikscode.com/blogs "稱為"請(qǐng)求",返回的客戶端列表稱為響應(yīng)。每個(gè)請(qǐng)求由 4 個(gè)部分組成:
- 終結(jié)點(diǎn)(路由) – 這是我們之前提到的 URL。它的結(jié)構(gòu)是這樣的 - "根端點(diǎn)/?根終結(jié)點(diǎn)是起點(diǎn),后跟路徑和查詢參數(shù)。該路徑定義所需的特定資源。例如,GitHub 的 API 的根端點(diǎn)是"api.github.com",而我在 GitHub 上的存儲(chǔ)庫(kù)列表的完整端點(diǎn)是 https://api.github.com/users/nmzivkovic/repos。
- 方法 – 可以發(fā)送五種類型的請(qǐng)求,該方法定義此類型:GET – 用于獲取或讀取信息。開(kāi)機(jī)自檢 – 用于創(chuàng)建新資源。PUT 和 PATCH – 它們用于更新資源。刪除 – 刪除資源。
- 標(biāo)頭 – 標(biāo)頭用于以屬性值對(duì)的形式向客戶端和服務(wù)器提供其他信息。MDN的HTTP頭引用上的有效頭列表。
- 正文 – 此部分包含客戶端發(fā)送到服務(wù)器的信息。它不用于 GET 請(qǐng)求。
我們要在本文中創(chuàng)建的是 Web 服務(wù)器,它為 Iris 預(yù)測(cè)提供模型。我們希望構(gòu)建 API,應(yīng)用程序客戶端可以使用該 API 從模型中獲取預(yù)測(cè)。這是使用Python框架Flask完成的。
2. 快速API基礎(chǔ)知識(shí)
在本文中,我們使用 FastAPI 來(lái)構(gòu)建 REST API。我們?yōu)槭裁匆褂眠@個(gè)庫(kù)?這有幾個(gè)原因。與其他主要的Python框架(如Flask和Django)相比,FastAPI更快。此外,此框架還支持使用 async/await Python 關(guān)鍵字的開(kāi)箱即用異步代碼。這進(jìn)一步提高了其性能。使FastAPI成為最好的API庫(kù)之一的最顯著的功能可能是內(nèi)置的交互式文檔。稍后我們將更詳細(xì)地探討此功能。最后,使用 FastAPI 構(gòu)建的應(yīng)用程序非常易于測(cè)試和部署。由于所有這些,FastAPI成為使用Python構(gòu)建Web API應(yīng)用程序的標(biāo)準(zhǔn)。
2.1 首次快速API應(yīng)用
好了,讓我們構(gòu)建第一個(gè) FastAPI 應(yīng)用程序。很酷的事情是,一個(gè)簡(jiǎn)單的HelloWorld示例與FastAPI可以用5行代碼創(chuàng)建。我不是在開(kāi)玩笑。準(zhǔn)備好了嗎?以下是您需要執(zhí)行的操作:
- 創(chuàng)建一個(gè)新文件夾并將其命名為"my_first_fastapi"(或者您認(rèn)為:)的任何內(nèi)容)
- 創(chuàng)建一個(gè)名為 main.py 的新Python腳本(或者你覺(jué)得它:)的任何內(nèi)容)
- 將以下代碼添加到 main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
- 轉(zhuǎn)到終端并放入my_first_fastapi文件夾中
- 運(yùn)行以下命令:
uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.
太棒了,現(xiàn)在你有一個(gè)Web服務(wù)器在 http://127.0.0.1:8000 運(yùn)行。如果您在瀏覽器中轉(zhuǎn)到此地址,您將看到如下內(nèi)容:
請(qǐng)注意,在上面的代碼中@app.get("route") 裝飾器?此修飾器確定可以在特定終結(jié)點(diǎn)上發(fā)出的請(qǐng)求類型。這意味著,例如,如果我們放置 app.get("/users"),我們將 REST API 端點(diǎn)定義為"users",我們可以在其上發(fā)出 GET http 請(qǐng)求。
2.2 快速 API 文檔
FastAPI最酷的事情之一是內(nèi)置的交互式文檔。它以 Swagger UI 呈現(xiàn)。如果我們使用剛剛構(gòu)建的示例并轉(zhuǎn)到
http://127.0.0.1:8000/docs 我們將看到 API 的文檔頁(yè)面:
您可以單擊任何端點(diǎn),進(jìn)一步瀏覽并了解它們。本文檔的最大好處可能是,您可以通過(guò)單擊"試用"按鈕來(lái)執(zhí)行實(shí)時(shí)瀏覽器內(nèi)測(cè)試。
我知道,對(duì)吧?!
好了,回到我們的應(yīng)用程序,讓我們看看如何利用 FastAPI 進(jìn)行機(jī)器學(xué)習(xí)部署。
3. 客戶端和用戶界面
好的,為了讓用戶與我們的模型進(jìn)行通信,我們需要某種用戶界面。有許多可用的框架和技術(shù)可供您使用。在這里,您可以看到我們?nèi)绾斡?strong>Flask和Python做類似的事情。在本教程中,我們使用 Angular 構(gòu)建用戶可以與之交互的 Web 應(yīng)用程序。
我們不會(huì)在這里過(guò)多地討論實(shí)現(xiàn)的細(xì)節(jié),因?yàn)橹攸c(diǎn)是FastAPI和機(jī)器學(xué)習(xí)模型。從本質(zhì)上講,應(yīng)用程序Train和Predict的工具欄中有兩個(gè)項(xiàng)目,它將我們引導(dǎo)到兩個(gè)具有相同名稱的網(wǎng)頁(yè)。完整的內(nèi)容在兩個(gè)組件中實(shí)現(xiàn):訓(xùn)練組件和預(yù)測(cè)組件。還有一項(xiàng)服務(wù) - fastapi.service。此服務(wù)包含對(duì)服務(wù)器端實(shí)現(xiàn)的 REST API 的調(diào)用。
在訓(xùn)練選項(xiàng)卡中,您可以選擇要訓(xùn)練的模型,選擇要試驗(yàn)的數(shù)據(jù)(采用類似 PalmerPenguing 數(shù)據(jù)集的格式),以及定義測(cè)試數(shù)據(jù)集的大小。按下訓(xùn)練按鈕后,包含此信息的 HTTP 請(qǐng)求將發(fā)送到服務(wù)器端,我們希望它將訓(xùn)練定義的機(jī)器學(xué)習(xí)模型。
在"預(yù)測(cè)"選項(xiàng)卡中,用戶可以使用通過(guò)上一個(gè)選項(xiàng)卡訓(xùn)練的機(jī)器學(xué)習(xí)模型進(jìn)行預(yù)測(cè)。在這里,用戶可以輸入描述企鵝的參數(shù),用戶希望對(duì)此進(jìn)行預(yù)測(cè)。按下"預(yù)測(cè)"按鈕后,此信息將發(fā)送到服務(wù)器,該服務(wù)器使用機(jī)器學(xué)習(xí)模型進(jìn)行預(yù)測(cè)并將其發(fā)送回用戶。
要運(yùn)行此應(yīng)用程序(我們假設(shè)您已經(jīng)克隆了 GitHub 存儲(chǔ)庫(kù)),請(qǐng)打開(kāi)終端并放入 train_solutionclient 并運(yùn)行以下命令:
npm install
ng serve
完成此操作后,此 Web 應(yīng)用將在 localhost:4200 上可用。好吧,讓我們看看服務(wù)器端實(shí)現(xiàn)的樣子。
3. 模型訓(xùn)練解決方案
服務(wù)器解決方案由多個(gè)組件組成,這些組件可以在train_solutionserver 文件夾中的文件中找到。整體架構(gòu)如下所示:
讓我們來(lái)探索每個(gè)組件。
3.1 數(shù)據(jù)合同
train_parameters.py文件包含從 Web 應(yīng)用程序的"訓(xùn)練"選項(xiàng)卡傳遞的模型。從本質(zhì)上講,此文件包含一個(gè)數(shù)據(jù)協(xié)定,當(dāng)來(lái)自客戶端的HTTP請(qǐng)求到達(dá)我們的服務(wù)器時(shí),將使用該協(xié)定。
from pydantic import BaseModel
class TrainParameters(BaseModel):
model: str
path: str
testsize: float
請(qǐng)注意,我們使用 Pydantic、數(shù)據(jù)驗(yàn)證和設(shè)置管理庫(kù)進(jìn)行類型注釋。此庫(kù)在運(yùn)行時(shí)強(qiáng)制實(shí)施類型提示,并在數(shù)據(jù)無(wú)效時(shí)提供用戶友好的錯(cuò)誤。我們?cè)?em>penguin_sample.py中使用的同一庫(kù),其中我們定義了從Web應(yīng)用程序的預(yù)測(cè)頁(yè)面發(fā)送HTTP請(qǐng)求時(shí)使用的數(shù)據(jù)協(xié)定:
from pydantic import BaseModel
class PenguinSample(BaseModel):
island: str
culmenLength: float
culmenDepth: float
flipperLength: float
bodyMass: float
sex: str
species: str
def __getitem__(self, item):
return getattr(self, item)
3.2 數(shù)據(jù)加載器
此類用于加載和準(zhǔn)備數(shù)據(jù)。這是它的樣子:
import pandas as pd
import numpy as np
from scipy.stats import norm
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from penguin_sample import PenguinSample
class DataLoader():
def __init__(self, test_size = 0.2, scale = True):
self.test_size = test_size
self.scale = scale
self.scaler = StandardScaler()
self._island_map = {}
self._sex_map = {}
def load_preprocess(self, path):
data = pd.read_csv(path)
data = self._feature_engineering_pipeline(data)
X = data.drop(['species', "island", "sex"], axis=1)
if(self.scale):
X = self.scaler.fit_transform(X)
y = data['species']
spicies = {'Adelie': 0, 'Chinstrap': 1, 'Gentoo': 2}
y = [spicies[item] for item in y]
y = np.array(y)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=self.test_size,
random_state=33)
return X_train, X_test, y_train, y_test
def prepare_sample(self, raw_sample: PenguinSample):
island = self._island_map[raw_sample.island]
sex = self._sex_map[raw_sample.sex]
sample = [raw_sample.culmenLength, raw_sample.culmenDepth, raw_sample.flipperLength,
raw_sample.bodyMass, island, sex]
sample = np.array([np.asarray(sample)]).reshape(-1, 1)
if(self.scale):
self.scaler.fit_transform(sample)
return sample.reshape(1, -1)
def _feature_engineering_pipeline(self, data):
data['culmen_length_mm'].fillna((data['culmen_length_mm'].mean()), inplace=True)
data['culmen_depth_mm'].fillna((data['culmen_depth_mm'].mean()), inplace=True)
data['flipper_length_mm'].fillna((data['flipper_length_mm'].mean()), inplace=True)
data['body_mass_g'].fillna((data['body_mass_g'].mean()), inplace=True)
data["species"] = data["species"].astype('category')
data["island"] = data["island"].astype('category')
data["sex"] = data["sex"].astype('category')
data["island_cat"] = data["island"].cat.codes
data["sex_cat"] = data["sex"].cat.codes
self._island_map = dict(zip(data['island'], data['island'].cat.codes))
self._sex_map = dict(zip(data['sex'], data['sex'].cat.codes))
return data
作為構(gòu)造函數(shù)中的參數(shù),它接收測(cè)試數(shù)據(jù)集大小和指示數(shù)據(jù)是否應(yīng)縮放的標(biāo)志。此類中有兩個(gè)公共方法和一個(gè)私有方法:
- _feature_engineering_pipeline – 此方法在現(xiàn)有集合上進(jìn)行小型特征工程。也就是說(shuō),填充缺失的數(shù)據(jù)并對(duì)分類數(shù)據(jù)進(jìn)行編碼。如果您想了解有關(guān)功能工程的更多信息,請(qǐng)查看此博客文章。
- load_preprocess – 此方法執(zhí)行繁重的工作,它從定義的路徑加載數(shù)據(jù),并將數(shù)據(jù)拆分為訓(xùn)練和測(cè)試數(shù)據(jù)集。
- prepare_sample – 當(dāng)新樣本進(jìn)入我們的系統(tǒng)時(shí),我們需要以與處理訓(xùn)練數(shù)據(jù)相同的方式處理它。這就是為什么我們?cè)谶M(jìn)行進(jìn)一步預(yù)測(cè)之前使用prepare_sample方法的原因。
3.3 模型訓(xùn)練器
此組件負(fù)責(zé)訓(xùn)練模型。通過(guò)構(gòu)造函數(shù),它接收有關(guān)用戶選擇的算法的信息,并從那里開(kāi)始處理它。這是它的樣子:
from data_loader import DataLoader
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from penguin_sample import PenguinSample
class ModelTrainer():
def __init__(self, algoritm, test_size=0.2):
if(algoritm == 'svm'):
self.data_loader = DataLoader()
self.model = SVC(kernel="rbf", gamma=0.1, C=500, verbose=True)
if(algoritm == 'logistic regression'):
self.data_loader = DataLoader()
self.model = LogisticRegression(C=1e20, verbose=True)
if(algoritm == 'decision tree'):
self.data_loader = DataLoader(scale = False)
self.model = DecisionTreeClassifier(max_depth=5)
if(algoritm == 'random forest'):
self.data_loader = DataLoader(scale = False)
self.model = RandomForestClassifier(n_estimators=11, max_leaf_nodes=16, n_jobs=-1,
verbose=True)
def train(self, path):
X_train, X_test, y_train, y_test = self.data_loader.load_preprocess(path)
self.model.fit(X_train, y_train)
predictions = self.model.predict(X_test)
return accuracy_score(predictions, y_test)
def predict(self, data: PenguinSample):
prepared_sample = self.data_loader.prepare_sample(data)
return self.model.predict(prepared_sample)
在構(gòu)造函數(shù)中,實(shí)例化了正確的模型。如您所見(jiàn),使用了SciKit Learn中的類。除此之外,還創(chuàng)建了DataLoader的一個(gè)實(shí)例。在訓(xùn)練方法中,檢索數(shù)據(jù)并訓(xùn)練模型。完成此操作后,將計(jì)算準(zhǔn)確性分?jǐn)?shù),并將該值返回給調(diào)用方。另一方面,預(yù)測(cè)方法接收新示例,并使用數(shù)據(jù)加載程序?qū)嵗龑⑵?strong>調(diào)整到模型。然后調(diào)用 predict 方法,并將結(jié)果返回給調(diào)用方。
3.4 REST API 模塊
最重要的文件是 main.py 文件。此文件將所有其他部分放在一起,并使用 FastAPI 構(gòu)建 REST API。這是看起來(lái)的樣子:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from model_trainer import ModelTrainer
from train_parameters import TrainParameters
from penguin_sample import PenguinSample
origins = [
"http://localhost:8000",
"http://localhost:4200"
]
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.model = ModelTrainer('svm')
@app.post("/train")
async def train(params: TrainParameters):
print("Model Training Started")
app.model = ModelTrainer(params.model.lower(), params.testsize)
accuracy = app.model.train(params.path)
return accuracy
@app.post("/predict")
async def predict(data:PenguinSample):
print("Predicting")
spicies_map = {0: 'Adelie', 1: 'Chinstrap', 2: 'Gentoo'}
species = app.model.predict(data)
return spicies_map[species[0]]
首先,我們導(dǎo)入所有必要的庫(kù)。由于我們的服務(wù)器在 http://localhost:8000 和客戶端中以 http://localhost:4200 運(yùn)行,因此我們需要處理 CORS 策略。這就是我們導(dǎo)入 CORSMiddleware 的原因。此類在應(yīng)用初始化期間使用:
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.model = ModelTrainer('svm')
請(qǐng)注意,我們?cè)谀抢锾砑恿艘粋€(gè) ModelTrainer 的實(shí)例。此實(shí)例只是一個(gè)占位符,稍后將替換。有兩個(gè)終結(jié)點(diǎn)"/trian"和"/predict"都接受POST HTTP請(qǐng)求。這樣,我們有兩個(gè)函數(shù)來(lái)處理這些請(qǐng)求。訓(xùn)練方法根據(jù)從客戶端收到的參數(shù)創(chuàng)建 ModelTrainer 的新實(shí)例。然后,它運(yùn)行模型的訓(xùn)練并返回模型的準(zhǔn)確性:
@app.post("/train")
async def train(params: TrainParameters):
print("Model Training Started")
app.model = ModelTrainer(params.model.lower(), params.testsize)
accuracy = app.model.train(params.path)
return accuracy
預(yù)測(cè)方法是從 Web 應(yīng)用程序的預(yù)測(cè)選項(xiàng)卡接收數(shù)據(jù)。它從 ModelTrainer 調(diào)用 predict 方法并返回預(yù)測(cè)值:
@app.post("/predict")
async def predict(data:PenguinSample):
print("Predicting")
spicies_map = {0: 'Adelie', 1: 'Chinstrap', 2: 'Gentoo'}
species = app.model.predict(data)
return spicies_map[species[0]]
3.5 測(cè)試服務(wù)器端
要運(yùn)行此解決方案,請(qǐng)打開(kāi)終端,然后轉(zhuǎn)到train_solutionserver 文件夾。運(yùn)行以下命令:
uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.
應(yīng)用程序運(yùn)行后,轉(zhuǎn)到瀏覽器中的localhost:8000 / docs。您應(yīng)該看到如下內(nèi)容:
首先,讓我們測(cè)試一下訓(xùn)練終結(jié)點(diǎn)。展開(kāi)它,然后單擊"試用"按鈕:
作為請(qǐng)求正文傳遞此 json 對(duì)象:
{
"model": "svm",
"path": "./data/penguins_size.csv",
"testsize": 0.2
}
然后點(diǎn)擊 執(zhí)行 按鈕:
以下是我們得到的回應(yīng):
以類似的方式,我們可以測(cè)試預(yù)測(cè)端點(diǎn)。試試吧!
3.6 一起運(yùn)行
要運(yùn)行服務(wù)器端,您需要轉(zhuǎn)到train_solutionserver文件夾并使用以下命令:
uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.
在另一個(gè)終端中,您需要轉(zhuǎn)到train_solutionclient文件夾并運(yùn)行:
npm install
ng serve
√ Browser application bundle generation complete.
Initial Chunk Files | Names | Size
vendor.js | vendor | 3.02 MB
polyfills.js | polyfills | 481.27 kB
styles.css, styles.js | styles | 340.83 kB
main.js | main | 93.72 kB
runtime.js | runtime | 6.15 kB
| Initial Total | 3.92 MB
Build at: 2020-11-22T10:33:00.423Z - Hash: bf345d81dfd56983facb - Time: 10867ms
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
√ Compiled successfully.
完成此操作后,您可以轉(zhuǎn)到localhost:4200并測(cè)試應(yīng)用程序:
4. 模型加載解決方案
現(xiàn)在,以前的解決方案真的很漂亮。我真的很喜歡你可以選擇不同的模型并玩轉(zhuǎn)參數(shù)。然而,即使它是底層的,它也不是一個(gè)現(xiàn)實(shí)世界的例子,它更像是一個(gè)虛榮的項(xiàng)目。在實(shí)際解決方案中,一個(gè)組件負(fù)責(zé)收集數(shù)據(jù),另一個(gè)組件負(fù)責(zé)處理該數(shù)據(jù),第三個(gè)組件負(fù)責(zé)定期訓(xùn)練模型并將其存儲(chǔ)在某個(gè)位置。然后,REST API 會(huì)利用這些存儲(chǔ)的模型。現(xiàn)在,所有這些對(duì)于這個(gè)簡(jiǎn)單的教程來(lái)說(shuō)都太多了,但是,我仍然需要給出一些更接近現(xiàn)實(shí)世界的問(wèn)題和解決方案的東西。因此,服務(wù)器端的體系結(jié)構(gòu)更改為:
這些更改也會(huì)影響 UI。火車選項(xiàng)卡變成了"加載"選項(xiàng)卡,它更簡(jiǎn)單一些。沒(méi)有測(cè)試大小定義,也沒(méi)有數(shù)據(jù)路徑定義。但是,用戶仍然可以選擇要使用的模型。該解決方案位于load_solution/客戶端路徑中,如下所示:
服務(wù)器端位于load_solution/服務(wù)器文件夾中,它的變化更多一些。讓我們來(lái)看看每個(gè)組件。
4.1 訓(xùn)練和保存模型
您可能已經(jīng)注意到解決方案文件夾中的新文件夾"models"。在此文件夾中,您可以找到以下文件:
這些是已經(jīng)訓(xùn)練過(guò)的模型。實(shí)際上,有一個(gè)腳本train_models_script.py,如果要再次重新訓(xùn)練以下模型,則可以運(yùn)行該腳本。它利用了先前實(shí)現(xiàn)中的零碎部分,但有一個(gè)主要區(qū)別。模型不存儲(chǔ)在內(nèi)存中,而是存儲(chǔ)在硬設(shè)備中。腳本如下:
from sklearn.ensemble import RandomForestClassifier
from joblib import dump
def load_data(scale = True):
data = pd.read_csv('./data/penguins_size.csv')
data['culmen_length_mm'].fillna((data['culmen_length_mm'].mean()), inplace=True)
data['culmen_depth_mm'].fillna((data['culmen_depth_mm'].mean()), inplace=True)
data['flipper_length_mm'].fillna((data['flipper_length_mm'].mean()), inplace=True)
data['body_mass_g'].fillna((data['body_mass_g'].mean()), inplace=True)
data["species"] = data["species"].astype('category')
data["island"] = data["island"].astype('category')
data["sex"] = data["sex"].astype('category')
data["island_cat"] = data["island"].cat.codes
data["sex_cat"] = data["sex"].cat.codes
X = data.drop(['species', "island", "sex"], axis=1)
if(scale):
scaler = StandardScaler()
X = scaler.fit_transform(X)
y = data['species']
spicies = {'Adelie': 0, 'Chinstrap': 1, 'Gentoo': 2}
y = [spicies[item] for item in y]
y = np.array(y)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=33)
return X_train, X_test, y_train, y_test
# Train SVM
X_train, X_test, y_train, y_test = load_data(scale=True)
model = SVC(kernel="rbf", gamma=0.1, C=500, verbose=True)
model.fit(X_train, y_train)
dump(model, './models/svm.joblib')
# Train Decision Tree
X_train, X_test, y_train, y_test = load_data(scale=False)
model = DecisionTreeClassifier(max_depth=5)
model.fit(X_train, y_train)
dump(model, './models/decision_tree.joblib')
# Train Random Forest
X_train, X_test, y_train, y_test = load_data(scale=False)
model = RandomForestClassifier(n_estimators=11, max_leaf_nodes=16, n_jobs=-1, verbose=True)
model.fit(X_train, y_train)
dump(model, './models/random_forest.joblib')
# Train Logistic Regression
X_train, X_test, y_train, y_test = load_data(scale=True)
model = LogisticRegression(C=1e20, verbose=True)
model.fit(X_train, y_train)
dump(model, './models/logistic_regression.joblib')
load_data函數(shù)加載數(shù)據(jù)并執(zhí)行所有必要的預(yù)處理,就像 DataLoader 在上一個(gè)解決方案中所做的那樣。然后,我們使用 joblib 的轉(zhuǎn)儲(chǔ)函數(shù)訓(xùn)練模型并將其保存在文件中。您可以按如下方式運(yùn)行此腳本:
python train_models_script.py
4.2 模型加載器
此組件看起來(lái)類似于先前實(shí)現(xiàn)中的 ModelTrainer,但它更簡(jiǎn)單。這是它的樣子:
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from penguin_sample import PenguinSample
from sklearn.preprocessing import StandardScaler
from joblib import load
import numpy as np
class ModelLoader():
def __init__(self, algoritm):
self.scaledData = True
self._island_map = {'Torgersen': 2, 'Biscoe': 0, 'Dream': 1}
self._sex_map = {'MALE': 2, 'FEMALE': 1}
if(algoritm == 'svm'):
self.model = load('./models/svm.joblib')
if(algoritm == 'logistic regression'):
self.model = load('./models/decision_tree.joblib')
if(algoritm == 'decision tree'):
self.scaledData = False
self.model = load('./models/random_forest.joblib')
if(algoritm == 'random forest'):
self.scaledData = False
self.model = load('./models/logistic_regression.joblib')
self.scaler = StandardScaler()
def prepare_sample(self, raw_sample: PenguinSample):
island = self._island_map[raw_sample.island]
sex = self._sex_map[raw_sample.sex]
sample = [raw_sample.culmenLength, raw_sample.culmenDepth, raw_sample.flipperLength,
raw_sample.bodyMass, island, sex]
sample = np.array([np.asarray(sample)]).reshape(-1, 1)
if(self.scaledData):
self.scaler.fit_transform(sample)
return sample.reshape(1, -1)
def predict(self, data: PenguinSample):
prepared_sample = self.prepare_sample(data)
return self.model.predict(prepared_sample)
同樣,根據(jù)我們從客戶端接收的參數(shù),我們使用 joblib 的 load 函數(shù)加載正確的模型。這兩種方法用于執(zhí)行預(yù)測(cè)。prepare_sample準(zhǔn)備一個(gè)新樣本進(jìn)行模型處理,而預(yù)測(cè)方法則在模型中運(yùn)行樣本并檢索預(yù)測(cè)。
4.3 REST API 模塊
此模塊與之前的實(shí)現(xiàn)幾乎相同:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from model_loader import ModelLoader
from train_parameters import TrainParameters
from penguin_sample import PenguinSample
origins = [
"http://localhost:8000",
"http://127.0.0.1:8000/predict",
"http://127.0.0.1:8000/load",
"http://localhost:4200"
]
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.model = ModelLoader('svm')
@app.post("/train")
async def train(params: TrainParameters):
print("Model Loading Started")
app.model = ModelLoader(params.model.lower())
return True
@app.post("/predict")
async def predict(data:PenguinSample):
print("Predicting")
spicies_map = {0: 'Adelie', 1: 'Chinstrap', 2: 'Gentoo'}
species = app.model.predict(data)
return spicies_map[species[0]]
主要區(qū)別在于使用了 ModelLoader 類,并且沒(méi)有對(duì)模型進(jìn)行任何訓(xùn)練。
要運(yùn)行服務(wù)器端,您需要轉(zhuǎn)到train_solutionserver文件夾并使用以下命令:
4.4 一起運(yùn)行
要運(yùn)行服務(wù)器端,您需要轉(zhuǎn)到load_solutionserver文件夾并使用以下命令:
uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.
在另一個(gè)終端中,您需要轉(zhuǎn)到load_solutionclient文件夾并運(yùn)行:
npm install
ng serve
√ Browser application bundle generation complete.
Initial Chunk Files | Names | Size
vendor.js | vendor | 3.02 MB
polyfills.js | polyfills | 481.27 kB
styles.css, styles.js | styles | 340.83 kB
main.js | main | 93.72 kB
runtime.js | runtime | 6.15 kB
| Initial Total | 3.92 MB
Build at: 2020-11-22T10:33:00.423Z - Hash: bf345d81dfd56983facb - Time: 10867ms
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
√ Compiled successfully.
完成此操作后,您可以轉(zhuǎn)到localhost:4200并測(cè)試應(yīng)用程序:
結(jié)論
在本文中,我們能夠看到如何使用 FastAPI 和一些 JavaScript 框架(在此特定情況下為 Angular)部署機(jī)器學(xué)習(xí)算法。我們看到了什么是REST API以及如何使用FastAPI構(gòu)建它。最后,我們?cè)谟脩艚缑娴膸椭吕昧诉@個(gè)API,完成了整個(gè)解決方案。如果你想更進(jìn)一步,你可能想把它放到Docker實(shí)例中,并使用Kubernetes。
感謝您的閱讀!
原文標(biāo)題:Deploying machine Learning Models with FastAPI and Angular
作者:AI, Angular, Machine Learning, Python
原文:
https://rubikscode.net/2020/11/23/deploying-machine-learning-models-with-fastapi-and-angular/
編譯:LCR