Deploy Machine Learning Model บน Production ด้วย FastAPI, Uvicorn และ Docker
Architecture
สถาปัตยกรรมใน Workshop นี้ จะมีโครงสร้างดังภาพด้านล่าง จากภาพ HTTP Traffic ที่มาจาก Internet จะวิ่งไปยัง Uvicon Server ที่ Port 7001 โดย Uvicon จะทำหน้าที่ในการรัน Python Web Application แบบ Asynchronous Process ที่มีการพัฒนาด้วย FastAPI Framework โดยมี /getclass เป็น API Endpoint (http://hostname:7001/getclass) ซึ่งมีการรับข้อมูลเป็น JSON Format (จาก HTTP POST Method) แล้วส่งผลการ Predict ด้วย Model ที่พัฒนาโดยใช้ Tensorflow Framework กลับมาเป็น JSON Format เช่นเดียวกัน ซึ่งเราจะเห็นว่า Component ทั้งหมดที่กล่าวมานั้นจะถูกบรรจุอยู่ภายใน Docker Container เพียง Container เดียว
Project Structure
ยกตัวอย่างด้วยการ Train Neural Network Model อย่างง่ายเพื่อจำแนกข้อมูล 3 Class แล้ว Save Model เป็นไฟล์ model1.h5 เพื่อนำไปบรรจุลงใน Docker Container โดยไฟล์ทั้งหมดใน Project นี้ จะจัดเก็บใน Folder ชื่อ basic_model ซึ่งภายใน basic_model จะประกอบด้วยไฟล์ และ Folder ดังต่อไปนี้
├── model_deploy
│ ├── docker-compose.yml
│ └── python
│ ├── Dockerfile
│ ├── api.py
│ ├── model1.h5
│ ├── .env
│ └── requirements.txt
└── train_model
├── train_classification_model.ipynb
├── model1.h5
└── loadtest.py
สร้าง Environment ใหม่ ตั้งชื่อเป็น basic_model สำหรับรัน Python 3.6.8 และติดตั้ง Library ที่จำเป็น
conda create -n basic_model python=3.6.8 fastapi uvicorn python-dotenv pydantic locust plotly scikit-learn seaborn jupyter -c conda-forge
ลบ Environment ที่เคยสร้าง
conda remove — name basic_model — all
import library ที่จำเป็น
import matplotlib.pyplot as plt
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequentialfrom tensorflow.keras.utils import to_categorical
from sklearn.datasets import make_blobsfrom sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_reportimport pandas as pd
import plotly.express as pximport plotly
import plotly.graph_objs as goimport seaborn as sn
import numpy as np
แบ่ง Dataset เป็น 2 ส่วน สำหรับการ Train 60% และสำหรับการ Test อีก 40%
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, shuffle= True)
X_train.shape, X_test.shape, y_train.shape, y_test.shape
นำ Dataset ส่วนที่ Train มาแปลงเป็น DataFrame โดยเปลี่ยนชนิดข้อมูลใน Column “class” เป็น String
X_train_pd = pd.DataFrame(X_train, columns=[‘x’, ‘y’])
y_train_pd = pd.DataFrame(y_train, columns=[‘class’])df = pd.concat([X_train_pd, y_train_pd], axis=1)
df[“class”] = df[“class”].astype(str)fig = px.scatter(df, x=”x”, y=”y”, color=”class”)
fig.show()
นิยาม, Compile และ Train Model
model = Sequential()
model.add(Dense(50, input_dim=2, activation=’relu’, kernel_initializer=’he_uniform’))
model.add(Dense(3, activation=’softmax’))model.compile(loss=’categorical_crossentropy’, optimizer=’adam’, metrics=[‘accuracy’])
his = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=200, verbose=1)
Plot Loss
plotly.offline.init_notebook_mode(connected=True)
h1 = go.Scatter(y=his.history[‘loss’],
mode=”lines”, line=dict(
width=2,
color=’blue’),
name=”loss”
)
h2 = go.Scatter(y=his.history[‘val_loss’],
mode=”lines”, line=dict(
width=2,
color=’red’),
name=”val_loss”
)data = [h1,h2]
layout1 = go.Layout(title=’Loss’,
xaxis=dict(title=’epochs’),
yaxis=dict(title=’’))
fig1 = go.Figure(data = data, layout=layout1)
plotly.offline.iplot(fig1, filename=”Intent Classification”)
Plot Accuracy
h1 = go.Scatter(y=his.history[‘accuracy’],
mode=”lines”, line=dict(
width=2,
color=’blue’),
name=”acc”)
h2 = go.Scatter(y=his.history[‘val_accuracy’],
mode=”lines”, line=dict(
width=2,
color=’red’),
name=”val_acc” )data = [h1,h2]
layout1 = go.Layout(title=’Accuracy’,
xaxis=dict(title=’epochs’),
yaxis=dict(title=’’))
fig1 = go.Figure(data = data, layout=layout1)
plotly.offline.iplot(fig1, filename=”Intent Classification”)
Model Predict จาก Test Dataset
predicted_classes = model.predict_classes(X_test)
predicted_classes.shape
เตรียมผลเฉลยของ Test Dataset สำหรับสร้างตาราง Confusion Matrix
y_true = np.argmax(y_test,axis = 1)
y_true.shape
คำนวณค่า Confusion Matrix
cm = confusion_matrix(y_true, predicted_classes)
แสดงตาราง Confusion Matrix ด้วย Heatmap
df_cm = pd.DataFrame(cm, range(3), range(3))
plt.figure(figsize=(10,7))
sn.set(font_scale=1.2)
sn.heatmap(df_cm, annot=True, fmt=’d’, annot_kws={“size”: 14})
plt.show()
แสดง Precision, Recall, F1-score
label = [‘0’, ‘1’, ‘2’]
print(classification_report(y_true, predicted_classes, target_names=label, digits=4))
Save Model
filepath=’model1.h5'
model.save(filepath)
Load Model
from tensorflow.keras.models import load_model
predict_model = load_model(filepath)
predict_model.summary()
Predict Model ที่ Load มา
a = np.array([[-2.521156, -5.015865]])
predict_model.predict(a)
Copy Model ที่ Train แล้วไปยัง Folder
cp model1.h5 C:\Users\Firstll\Desktop\basic_model\model_deploy\python
FastAPI and Uvicorn
แก้ไขไฟล์ api.py ด้วย Code Editor ของVisual Studio Code ตามตัวอย่างด้านล่าง
from tensorflow.keras.models import load_model
from fastapi import FastAPI
from pydantic import BaseModel
import numpy as npapp = FastAPI()
class Data(BaseModel):
x:float
y:floatdef loadModel():
global predict_modelpredict_model = load_model(‘model1.h5’)
loadModel()
async def predict(data):
classNameCat = {0:’class_A’, 1:’class_B’, 2:’class_C’}
X = np.array([[data.x, data.y]])pred = predict_model.predict(X)
res = np.argmax(pred, axis=1)[0]
category = classNameCat[res]
confidence = float(pred[0][res])
return category, confidence@app.post(“/getclass/”)
async def get_class(data: Data):
category, confidence = await predict(data)
res = {‘class’: category, ‘confidence’:confidence}
return {‘results’: res}
รัน Python Web Application (api.py) ด้วยคำส่ง uvicorn api:ap แล้วกด Allow
uvicorn api:app — host 0.0.0.0 — port 80 — reload
API Documentation
FastAPI จะสร้าง API Document ให้โดยอัตโนมัติ โดยเราสามารถทดลองใช้งาน API ได้จาก URL http://localhost/docs
ไปที่ /getclass แล้วกด Try it out>>แก้ไข Request body แบบ JSON Format กด Execute แล้วดูผลลัพธ์จากการ Predict
{
“x”: -2.521156,
“y”: -5.015865
}
Basic Authen
โดยผู้เขียนจะยกตัวอย่างการพิสูจน์ตัวตนแบบ Basic Authen ด้วย Username และ Password ดังต่อไปนี้
แก้ไขไฟล์ .env โดยการกำหนด Username และ Password ตามตัวอย่างด้านล่าง
API_USERNAME=rynlapat
API_PASSWORD=password
แก้ไขไฟล์ api.py
from tensorflow.keras.models import load_model
from fastapi import FastAPI
from pydantic import BaseModel
import numpy as npfrom fastapi import Depends, HTTPException
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette.status import HTTP_401_UNAUTHORIZED
import secrets
import os
from dotenv import load_dotenvload_dotenv(os.path.join(‘.env’))
API_USERNAME = os.getenv(“API_USERNAME”)
API_PASSWORD = os.getenv(“API_PASSWORD”)security = HTTPBasic()
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
correct_username = secrets.compare_digest(credentials.username, API_USERNAME)
correct_password = secrets.compare_digest(credentials.password, API_PASSWORD)
if not (correct_username and correct_password):
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail=’Incorrect username or password’,
headers={‘WWW-Authenticate’: ‘Basic’},
)
return credentials.usernameapp = FastAPI()
class Data(BaseModel):
x:float
y:floatdef loadModel():
global predict_modelpredict_model = load_model(‘model1.h5’)
loadModel()
async def predict(data):
classNameCat = {0:’class_A’, 1:’class_B’, 2:’class_C’}
X = np.array([[data.x, data.y]])pred = predict_model.predict(X)
res = np.argmax(pred, axis=1)[0]
category = classNameCat[res]
confidence = float(pred[0][res])
return category, confidence@app.post(“/getclass/”)
async def get_class(data: Data, username: str = Depends(get_current_username)):
category, confidence = await predict(data)
res = {‘class’: category, ‘confidence’:confidence}
return {‘results’: res}
ใช้งาน API อีกครั้ง โดยเมื่อเรากด Execute จะต้องมีการพิสูจน์ตัวตนด้วย Username และ Password
แก้ไขไฟล์ requirements.txt
python-dotenv
fastapi
uvicorn
pydantic
tensorflow
แก้ไขไฟล์ Dockerfile
FROM python:3.6.8-slim-stretch
RUN apt-get update && apt-get install -y python-pip \
&& apt-get clean
WORKDIR /app
COPY api.py .env model1.h5 requirements.txt ./
RUN pip install — no-cache-dir -r requirements.txt
CMD uvicorn api:app — host 0.0.0.0 — port 80 — workers 6
Load Testing with Locust
แก้ไขไฟล์ loadtest.py
from locust import HttpUser, task, between
import jsonclass QuickstartUser(HttpUser):
min_wait = 1000
max_wait = 2000 @task
def test_api(self): data = {“x”:-2.521156, “y”:-5.015865}
self.client.post(
url=”/getclass”,
data=json.dumps(data),
auth=(“nuttachot”, “password”))
ไปที่ Folder basic_model/train_model แล้วรันไฟล์ loadtest.py ด้วยคำสั่ง locust -f loadtest.py
locust -f loadtest.py — host=http://localhost:7001
กด Allow แล้วไปยัง URL http://localhost:8089 กำหนดจำนวน User และ Spawn rate เท่ากับ 20 แล้วกด Start swarming
ลองไป Load Testing ใน IP VM Address ที่ Port 8089