การ Deploy Machine Learning Model บน Production ด้วย FastAPI, Uvicorn และ Docker

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 ที่จำเป็น รวมทั้ง 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 pltfrom 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_splitfrom 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 snimport numpy as npX, y = make_blobs(n_samples=3000, centers=3, n_features=2, cluster_std=2, random_state=2)
นำ 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()

เข้ารหัสผลเฉลย แบบ One-Hot Encoding เพื่อที่ว่าเมื่อ Model มีการ Predict ว่าเป็น Class ไหน มันจะให้ค่าความมั่นใจ (Confidence) กลับมาด้วย
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
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)

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 ../model_deploy/python/
ขณะนี้ใน Project จะประกอบด้วยไฟล์ และ Folder ที่จำเป็นในการใช้งาน
FastAPI and Uvicorn
แก้ไขไฟล์ 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()
เข้าใช้ basic_model Environment โดยพิมพ์คำสั่ง conda activate ตามด้วยชื่อ Environment
conda activate basic_model
ไปที่ 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=UserName
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_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}
ทดลองใช้งาน 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 --host=http://localhost:7001
กด Allow แล้วไปยัง URL http://localhost:8089 กำหนดจำนวน User และ Spawn rate เท่ากับ 20 แล้วกด Start swarming


จะเห็นว่า API ของเรามี Response Time โดยเฉลี่ย (Median) เท่ากับ 46ms และจำนวน Request ต่อวินาทีเท่ากับ 12.5 Request
กดที่ Edit ทดลองเพิ่มจำนวน User และ Spawn rate เท่ากับ 50 แล้วกด Start swarming


จากภาพด้านบน เมื่อเพิ่มจำนวน User และ Spawn rate เท่ากับ 200 แล้ว Response Time เฉลี่ย (Median) จะเพิ่มเป็น 320ms แต่พบว่ามันจะไม่สามารถรองรับ Request ที่ยิงมาจาก Locust ได้เพิ่มขึ้นมากนัก (91.8 RPS)

จากกราฟที่มีลักษณะขึ้น ๆ ลง ๆ แสดงให้เห็นว่า API ของเราจะไม่สามารถรองรับ Request แบบสบายๆ ได้เหมือนปกติ
จะเห็นว่า เมื่อทำตามขั้นตอนทั้งหมดนี้เราก็สามารถ Deploy Model และทดสอบความสามารถในการรองรับ load ของ Model ที่จะขึ้นใช้งานจริงบน Production
Create Project In Git Lab
ขั้นตอนการนำ Model ที่สร้างไปทำงานใน Cloud Server โดยขั้นตอนแรก เราต้องเอา Model ของเราอัพขึ้นไปไหน Git ก่อนทำการ New Project ขึ้นมาตั้งชื่อว่า basic_model

จะได้ Project ดังนี้

ต่อมาเราก็จะมาทำการ Git เพื่อ Upload โฟลเดอร์ โดยเราจะอัพใน Visual Studio Code ด้วย Code ดั้งนี้
git config — global user.name “username” //ชื่อ username ที่สมัคร
git config — global user.email “email” // email ที่เราสมัคร
ใช้คำสั่งอัพโหลดไฟล์จาก Git
cd existing_folder
git init
git remote add origin http://gitlab.cpsudevops.com/Phromma_p/basic_model.git
git add .
git commit -m "Initial commit"
git push -u origin master
จากนั้นเราก็จะได้ไฟล์ของ model ที่อัพขึ้นบน Git
Create VM In Microsoft Azure
สมัครได้ที่ https://imagine.microsoft.comซึ่งพอสมัครเสร็จเราจะกดตรง Microsoft azure ซึ่งจะขึ้นมาหน้านี้

สร้าง Linux VM
เราจะทำการสร้าง Virtual machines กันโดยใช้ model ที่เราสร้างมกดตรงเพิ่ม Virtual machines

หลังจากทำการเพิ่มแล้วจะขึ้นให้เพิ่มข้อมูลตามหน้าจอแบบนี้

- Virtual machines name ซึ่งจะตั้งเป็นอะไรก็ได้พอใส่เสร็จจะขึ้นมาเป็นชื่อ Resource group ครับ
- Region ให้เลือกเป็น (Asia Pacific) Southeast Asia
- Image ให้เลือกเป็น Ubuntu Server 18.04
- Size ให้เลือกเป็น Standard_D2s_v3
- Authentication type ให้เลือกเป็น Password
- Username กับ Password อันนี้แล้วแต่เลยนะครับ *แต่ต้องจำให้ได้*
- Public inbound ports เลือก Allow Selected Port
- Select inbound ports เลือก SSH(22)

เสร็จแล้วจะได้หน้านี้

Clone Git Project
เป็นการนำไฟล์ model ที่อัพบน Git มาใช้งานใน VM
รันผ่าน command ซึ่งไปเอามาจาก clone with SSH
git clone http://gitlab.cpsudevops.com/Phromma_p/basic_model.git
Testing In FastAPI
FastAPI จะสร้าง API Document ให้โดยอัตโนมัติ โดยเราสามารถทดลองใช้งาน API ได้จาก URL http://localhost/docs ดังตัวอย่างต่อไปนี้

ไปที่ /getclass แล้วกด Try it out
แก้ไข Request body แบบ JSON Format กด Execute แล้วดูผลลัพธ์จากการ Predict
{
“x”: -2.521156,
“y”: -5.015865
}
Import Docker In VM Server
โดยจะลง Docker ผ่านคำสั่ง
sudo -i
snap install docker
ต่อมาจะอัพให้ทำงานผ่านตัว Docker โดยใช้คำสั่ง
sudo docker-compose up -d
หา Path ที่เป็นที่อยู่ไฟล์ docker-compose.yml ซึ่งใน Model ของเราอยู่ในโฟลเดอร์ชื่อ model_deploy
เช็คว่าอัพไปหรือไม่ โดยใช้คำสั่ง
sudo docker-compose ps
Import Locust In VM Server
ต่อมาเราต้องใช้ Locust ในการ Load Testing ของ Model ต้องติดตั้งตัว Python3-pip กับ Locust ตอนแรกติดตั้ง Python3-pip โดยใช้คำสั่ง
sudo -i
apt install python3-pip
Load Testing In Locast
ติดตั้ง locust ต่อโดยใช้
apt pip3 install locust
จากนั้นไปที่ Path basic_model/train_model แล้วก็จะใช้คำสั่ง
locust -f loadtest.py --host=http://localhost:7001
ทำการ Load Testing ด้วย IP address ของ Virtual machines Server ที่ Port 7001 เราจะ Testing โดยใส่ จำนวนลงในช่อง Number of total users to simulate และ Spawn rate แล้วกดดูผลได้เลย