我們花了將近一周的時間來介紹部署深度學習模型背後的概念,我想大家應該很想知道究竟該怎麼實作,所以今天就來動動手吧。
這部分的程式碼主要規劃為在本機端執行,所以在開始之前請先到 GitHub 下載檔案,並跟著頁面的說明先把虛擬環境建起來。
準備好之後,打開 server.ipynb
開始我們的冒險吧!!
今天我們要部署預先訓練好的 YOLOv4 模型進行物件偵測,而訓練資料為 MS COCO 資料集,其中包含了 80 種日常生活物品:
*圖片來源:COCO Explorer
因為今天的重點是部署,所以模型的部分直接使用簡單卻強大的物件偵測函式庫 cvlib,利用其 detect_common_objects 函式,它會接收格式為 numpy array 的圖片並回傳以下向量:
[[35, 80, 124, 188], [121, 91, 230, 177]]
['cat', 'dog']
[0.8760120272636414, 0.6513471007347107]
另外 detect_common_objects
還有一個很重要的輸入參數為信心閾值 confidence
,可以看到上方模型的輸出向量包含了圖片中不同物件是否存在的機率 (conf
),confidence
就是用來控制機率大於多少才判定為存在,其預設值為 0.5,調整此輸入參數的效果如下:
可以看到信心閾值為 0.5 時,模型少偵測了許多人、車,而降低信心閾值的確可以讓模型成功偵測到大部分的人、車,但為了正確偵測到圖片中的物件,我們需要將信心閾值設得很低。
在提高、降低這類參數時都要格外小心,因為改變它們的值有可能產生非預期的結果。
FastAPI 讓我們可以輕鬆建立網頁伺服器來 host 模型,而且除了速度很快以外,它還內建了能與伺服器互動的圖形化介面客戶端,只需要訪問 /docs
接口即可 (例如 http://localhost:8000/docs
)。
在對模型運作有基本的概念之後,我們準備進入重頭戲了,但在開始部署之前,先簡單複習一些重要的概念,以及 fastAPI 如何實現這些概念。
通常講到部署,實際上指的是把進行預測所需的全部軟體放進一個 伺服器(server) 中,如此一來,客戶端(client) 可以藉由將 請求(requests) 傳送到伺服器來與機器學習模型互動。
模型會在伺服器中等待客戶端發送的預測請求,因此請求必須包含模型進行預測所需的資訊,伺服器則會使用這些資訊來將預測回傳至客戶端。
通常一次 request 會包含數個預測需求。
首先,我們可以建立 FastAPI class 的實例 (instance):
app = FastAPI()
接著使用這個實例來建立負責處理預測之邏輯的接口 (endpoints),當所有程式碼都就定位之後,只需要執行以下指令就能啟動伺服器:
uvicorn.run(app)
我們的 API 是使用 fastAPI 編寫的,但上線 (serving) 是使用 uvicorn,它是速度非常快的非同步伺服器閘道界面 (Asynchronous Server Gateway Interface, ASGI),這兩樣技術會密切連接,但我們並不需要了解實作的細節,現在只需要知道 uvicorn 負責 serving 就夠了。
同一個伺服器可以 host 多個模型,只須為它們指派各自的接口即可隨意選用,而每個接口都是由 URL 的型式來代表。 假設我們有一個網站叫 myawesomemodel.com
,若上面有三個不同的模型,它們可分別位於以下接口:
myawesomemodel.com/count-cars/
myawesomemodel.com/count-apples/
myawesomemodel.com/count-plants/
而每個模型的功能就如其接口命名型式所示。
在 FastAPI 中,定義接口的步驟如下:
以下示範允許 HTTP GET 請求的接口 "/my-endpoint"
:
@app.get("/my-endpoint")
def handle_endpoint():
...
...
客戶端與伺服器藉由 HTTP 協定溝通,這些溝通會以動詞來標記它們的實際行動,其中兩個最常見的動詞為:
GET
:由伺服器取回資訊,如果客戶端對伺服器的某接口提出了 GET 請求,我們可以在不須提供額外資訊的情況下,由此接口得到一些資訊。POST
:提供需要做反應的資訊給伺服器,如果提出 POST 請求就代表明確的告訴伺服器我們會提供某些資訊,而它必須以某種方式處理這些資訊。通常都是以 POST
請求來與位於某接口的機器學習模型互動,因為我們需要提供資訊給它進行預測。
以下示範允許 HTTP POST 請求的接口 "/my-other-endpoint"
:
@app.post("/my-other-endpoint")
def handle_other_endpoint(param1: int, param2: str):
...
...
其中,允許POST 請求的 handler 函式會包含參數,因為它預期客戶提供某些資訊。
好啦,以上就是所有會用的基本知識,開始進入正題吧!!
首先引入必要的函式:
import io
import cv2
import cvlib as cv
from cvlib.object_detection import draw_bbox
import uvicorn
import numpy as np
import nest_asyncio
from enum import Enum
from fastapi import FastAPI, UploadFile, File, HTTPException, Query
from fastapi.responses import StreamingResponse
接著撰寫各個請求與預測的邏輯:
# Assign an instance of the FastAPI class to the variable "app".
# You will interact with your api using this instance.
app = FastAPI(title='使用 FastAPI 部署機器學習模型!!')
# List available models using Enum for convenience. This is useful when the options are pre-defined.
class Model(str, Enum):
yolov3tiny = "yolov3-tiny"
yolov3 = "yolov3"
yolov4tiny = "yolov4-tiny"
yolov4 = "yolov4"
# By using @app.get("/") you are allowing the GET method to work for the / endpoint.
@app.get("/", tags=["確認 API 是否成功運行"])
def home():
return "恭喜! 你的 API 成功運行中,去 http://localhost:8000/docs 看看吧!"
# This endpoint handles all the logic necessary for the object detection to work.
# It requires the desired model and the image in which to perform object detection.
@app.post("/predict", tags=["進行預測"])
def prediction(model: Model, confidence: float = Query(0.5, ge=0, le=1.0), file: UploadFile = File(...)):
# 1. VALIDATE INPUT FILE
filename = file.filename
fileExtension = filename.split(".")[-1] in ("jpg", "jpeg", "png")
if not fileExtension:
raise HTTPException(status_code=415, detail="Unsupported file provided.")
# 2. TRANSFORM RAW IMAGE INTO CV2 image
# Read image as a stream of bytes
image_stream = io.BytesIO(file.file.read())
# Start the stream from the beginning (position zero)
image_stream.seek(0)
# Write the stream of bytes into a numpy array
file_bytes = np.asarray(bytearray(image_stream.read()), dtype=np.uint8)
# Decode the numpy array as an image
image = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
# 3. RUN OBJECT DETECTION MODEL
# Run object detection
bbox, label, conf = cv.detect_common_objects(image, model=model, confidence=confidence)
# Create image that includes bounding boxes and labels
output_image = draw_bbox(image, bbox, label, conf)
# Save it in a folder within the server
cv2.imwrite(f'images_uploaded/{filename}', output_image)
# 4. STREAM THE RESPONSE BACK TO THE CLIENT
# Open the saved image for reading in binary mode
file_image = open(f'images_uploaded/{filename}', mode="rb")
# Return the image as a stream specifying media type
return StreamingResponse(file_image, media_type="image/jpeg")
最後,執行下面的程式碼就能將伺服器上線了:
# Allows the server to be run in this interactive environment
nest_asyncio.apply()
# Host depends on the setup you selected (docker or virtual env)
host = "0.0.0.0" if os.getenv("DOCKER-SETUP") else "127.0.0.1"
# Spin up the server!
uvicorn.run(app, host=host, port=8000)
伺服器已經上線了,可以到 http://localhost:8000/ 看看它是否成功運作,若成功運作應該可以看到以下畫面:
藉由訪問 http://localhost:8000/docs 可以進入 fastAPI 提供的內建 client,試試上傳圖片來看看我們的 API 如何偵測圖中的物件並回傳加上定界框與類別的圖片吧。
其介面如下,點擊 /predict 接口可以開啟更多選項,接著點擊 Try it out 就可以開始測試 API 了:
點開後可以在 model 欄位選取要使用的模型 (注意若選用 YOLO 模型需要等待一段時間讓權重下載完)。
點擊 file 欄位的 選擇檔案 則可以上傳想要進行辨識的圖片,最後點擊藍色 Execute 即可發送 HTTP 請求至伺服器,最後往下滑就可以看到辨識的結果了。
可以試試各種不同的圖片與信心閾值,當模型漏掉某些物件時可以試著降低閾值。
fastAPI 讓我們可以使用內建的 client 與 API 互動固然很棒,但其實我們也可以不使用 UI,直接用程式碼與其互動。
明天我們就會撰寫一個簡單的 client,然後使用它來與 API 互動,明天見啦。
如果等不及看完明天的內容,請保持伺服器執行,然後在另一個分頁打開 client.ipynb 筆記本吧。