การ Deploy Machine Learning Model บน Production ด้วย FastAPI, Uvicorn และ Docker
การพัฒนา Deep Learning Model ที่มีประสิทธิภาพพอจะใช้งานได้ระดับหนึ่งแล้ว สามารถนำ Model ไป Deploy บน Local Host หรือ Cloud Server น โดยมีการเรียกใช้ Model ผ่าน RestFul API เพื่อให้ผู้อ่านเห็นแนวทางการนำ Machine Learning Model ไปใช้งานจริง
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
Create a new Conda Environment
สร้าง Environment ชื่อ basic_model สำหรับรัน Python 3.6.8 โดยที่มี Package/Library ต่างๆ ได้แก่ FastAPI, Uvicorn, Python-dotenv, Pydantic, Tensorflow, Locust, Plotly, Scikit-learn, Seaborn รวมทั้ง Jupyter Notebook ซึ่งมีขั้นตอนดังต่อไปนี้
- สร้าง Environment ใหม่ ตั้งชื่อเป็น basic_model สำหรับรัน Python 3.6.8 และติดตั้ง Library ที่จำเป็น รวมทั้ง Jupyter Notebook โดยใช้คำสั่ง conda create -n
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
ก่อนลบ ออกจาก Environment ด้วยคำสั่ง
conda deavtivate
- เข้าใช้ Environment ใหม่ โดยพิมพ์คำสั่ง conda activate ตามด้วยชื่อ Environment
conda activate basic_model
- ติดตั้ง tensorflow
pip install tensorflow==2.3.0
- เปิด Jupyter Notebook
jupyter notebook
- ไปที่ Folder train_model สร้างไฟล์ใหม่ ชื่อไฟล์เป็น train_classification_model
Training and Save Model
ใช้ make_blobs() Function ของ scikit-learn Library ในการสร้าง Dataset ขนาด 2 มิติ ที่มีเพียง 3 Class ตามตัวอย่างด้านล่าง
- Import Library ที่จำเป็น แล้วสร้าง Dataset
import matplotlib.pyplot as plt
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential
from tensorflow.keras.utils import to_categorical
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
import pandas as pd
import plotly.express as px
import plotly
import plotly.graph_objs as go
import seaborn as sn
import numpy as npX, y = make_blobs(n_samples=3000, centers=3, n_features=2, cluster_std=2, random_state=2)
2. แบ่ง 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
3. นำ Dataset ส่วนที่ Train มาแปลงเป็น DataFrame โดยเปลี่ยนชนิดข้อมูลใน Column “class” เป็น String เพื่อทำให้สามารถแสดงสีแบบไม่ต่อเนื่องได้ แล้วนำไป Plot
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()
4. เข้ารหัสผลเฉลย แบบ One-Hot Encoding เพื่อที่ว่าเมื่อ Model มีการ Predict ว่าเป็น Class ไหน มันจะให้ค่าความมั่นใจ (Confidence) กลับมาด้วย
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
5. Define, 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)
6. 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")
7. 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")
8. Model Predict จาก Test Dataset
predicted_classes = model.predict_classes(X_test)
predicted_classes.shape
9. เตรียมผลเฉลยของ Test Dataset สำหรับสร้างตาราง Confusion Matrix
y_true = np.argmax(y_test,axis = 1)
y_true.shape
10. คำนวณค่า Confusion Matrix
cm = confusion_matrix(y_true, predicted_classes)
11. แสดงตาราง 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()
12. แสดง Precision, Recall, F1-score
label = ['0', '1', '2']
print(classification_report(y_true, predicted_classes, target_names=label, digits=4))
13. Save Model
filepath='model1.h5'
model.save(filepath)
14. ทดลอง Load Model
from tensorflow.keras.models import load_modelpredict_model = load_model(filepath)
predict_model.summary()
15. ทดลอง Predict จาก Model ที่ Load มาใหม่
a = np.array([[-2.521156, -5.015865]])predict_model.predict(a)
res = predict_model.predict(a)
np.argmax(res, axis=1)
16. Copy Model ที่ Train แล้วไปยัง Folder basic_model/model_deploy/python (บน Windows ใช้คำสั่ง %copy แทน cp)
cp model1.h5 ../model_deploy/python/
- ขณะนี้ใน Project จะประกอบด้วยไฟล์ และ Folder ที่จำเป็นในการใช้งานอย่างครบถ้วน
FastAPI and Uvicorn
ปัจจุบันมี Python Web Framework อยู่เป็นจำนวนมาก อย่างเช่น Flask, Falcon, Starlette, Sanic, Tornado และ FastAPI แต่ที่ FastAPI เป็นตัวเลือกอันดับต้นๆ สำหรับการพัฒนา RestFul API ก็เพราะมันสามารถเขียน Code ได้สั้นและเข้าใจง่ายเหมือนกับ Flask Framework แต่มีความเร็วที่สูงกว่า โดย FastAPI จะทำงานร่วมกับ Uvicorn Server (ASGI Server) ในการรองรับการทำงานแบบ Asynchronous รวมทั้งยังมีการสร้าง API Document ให้แบบอัตโนมัติ โดยมีขั้นตอนดังนี้
- แก้ไขไฟล์ api.py ด้วย 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_model predict_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}
จาก Code ด้านบน มีการนิยาม Function หลัก 3 Function ได้แก่
- loadModel()
- predict()
- get_class() ซึ่ง get_class() จะรับ Input Parameter แบบ JSON Format จาก HTTP POST Method ซึ่งมีการนิยามชนิดข้อมูลด้วย Pydantic Library
2. เข้าใช้ basic_model Environment โดยพิมพ์คำสั่ง conda activate ตามด้วยชื่อ Environment
conda activate basic_model
3. ไปที่ Folder basic_model/model_deploy/python รัน 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 = u5866107
API_PASSWORD = Watermelon37
2. แก้ไขไฟล์ 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_model predict_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}
2. ทดลองใช้งาน API อีกครั้ง โดยเมื่อเรากด Execute จะต้องมีการพิสูจน์ตัวตนด้วย Username และ Password ดังภาพด้านล่าง
Deployment
นำ Docker เข้ามาช่วยในการบรรจุ Software ทั้งหมดให้อยู่ในรูปของ Docker Image ซึ่งหลังจากนั้นก็จะนำ Docker Image ไปรัน (Docker Container) ในเครื่องไหนก็ได้ โดยทุกเครื่องจะมีสภาพแวดล้อมในการรันเหมือนกันทั้งหมด ไม่ว่าจะเป็นเครื่องสำหรับการ Development หรือ Production Server
เพื่อจะสร้าง Docker Container เราจะมีการแก้ไขไฟล์ docker-compose.yml, requirements.txt และ Dockerfile ดังต่อไปนี้
- แก้ไขไฟล์ docker-compose.yml
version: '3'services:
test_api:
container_name: test_api
build: python/
restart: always networks:
- default
ports:
- 7001:80
networks:
default:
external:
name: basic_model_network
- แก้ไขไฟล์ 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
- สร้าง Bridge network โดยตั้งชื่อเป็น basic_model_network ด้วยคำสั่ง docker network create บน Command Line
docker network create basic_model_network
- ไปที่ Folder basic_model/model_deploy แล้ว Build Image และ Run Container ด้วยคำสั่ง docker-compose up
docker-compose up -d
- ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps
Testing the API Request
กลับมาที่ Jupyter Notebook เพื่อทดลองเรียกใช้งาน API ด้วยคำสั่งต่อไปนี้
import requestsfrom requests.auth import HTTPBasicAuthURL = 'http://localhost:7001/getclass'data = {
"x": -2.521156,
"y": -5.015865
}response = requests.post(URL, json=data, auth=HTTPBasicAuth('nuttachot', 'password'))if response.status_code == 200:
res = response.json()['results']
print(res)
Load Testing with Locust
Locust เป็น Open Source Load Testing Framwork ที่มีการกำหนดวิธีการทดสอบด้วยการเขียน Script โดยใช้ Python Code
ในการทำ Load Testing ด้วย 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
จะเห็นว่า API มี Response Time โดยเฉลี่ย (Median) เท่ากับ 46ms และจำนวน Request ต่อวินาทีเท่ากับ 12.5 Request
- เพิ่มจำนวน User และ Spawn rate เท่ากับ 100 แล้วกด Start swarming
- Response Time เฉลี่ย (Median) จะเพิ่มเป็น 56ms และจำนวน Request ต่อวินาทีเท่ากับ 61.2 Request
- เพิ่มจำนวน User และ Spawn rate เท่ากับ 200 แล้วกด Start swarming
จากภาพด้านบน เมื่อเพิ่มจำนวน User และ Spawn rate เท่ากับ 200 แล้ว Response Time เฉลี่ย (Median) จะเพิ่มเป็น 320ms แต่พบว่ามันจะไม่สามารถรองรับ Request ที่ยิงมาจาก Locust ได้เพิ่มขึ้นมากนัก (91.8 RPS) และจากกราฟด้านล่างที่มีลักษณะขึ้น ๆ ลง ๆ แสดงให้เห็นว่า API จะไม่สามารถรองรับ Request ได้เหมือนปกติ
จะเห็นว่า เมื่อทำตามขั้นตอนทั้งหมดนี้จะสามารถ Deploy Model และทดสอบความสามารถในการรองรับ load ของ Model ที่จะขึ้นใช้งานจริงบน Production