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

Phromma P
8 min readSep 23, 2020

--

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 np
from 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_dotenv
load_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.username
app = FastAPI()class Data(BaseModel):
x:float
y:float
def 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

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

  1. Virtual machines name ซึ่งจะตั้งเป็นอะไรก็ได้พอใส่เสร็จจะขึ้นมาเป็นชื่อ Resource group ครับ
  2. Region ให้เลือกเป็น (Asia Pacific) Southeast Asia
  3. Image ให้เลือกเป็น Ubuntu Server 18.04
  4. Size ให้เลือกเป็น Standard_D2s_v3
  5. Authentication type ให้เลือกเป็น Password
  6. Username กับ Password อันนี้แล้วแต่เลยนะครับ *แต่ต้องจำให้ได้*
  7. Public inbound ports เลือก Allow Selected Port
  8. 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 แล้วกดดูผลได้เลย

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response