[Day 07] 使用 fastAPI 部署 YOLOv4 (1/2) — 以内建 Client 进行互动

前言

我们花了将近一周的时间来介绍部署深度学习模型背後的概念,我想大家应该很想知道究竟该怎麽实作,所以今天就来动动手吧。
这部分的程序码主要规划为在本机端执行,所以在开始之前请先到 GitHub 下载档案,并跟着页面的说明先把虚拟环境建起来。

准备好之後,打开 server.ipynb 开始我们的冒险吧!!

今天我们要部署预先训练好的 YOLOv4 模型进行物件侦测,而训练资料为 MS COCO 资料集,其中包含了 80 种日常生活物品:
coco
*图片来源:COCO Explorer

模型简介

因为今天的重点是部署,所以模型的部分直接使用简单却强大的物件侦测函式库 cvlib,利用其 detect_common_objects 函式,它会接收格式为 numpy array 的图片并回传以下向量:

  • bbox — 包含被侦测到物品定界框的 list,例如:
    [[35, 80, 124, 188], [121, 91, 230, 177]]
  • label — 包含被侦测到物件标签的 list,例如:
    ['cat', 'dog']
  • conf — 包含被侦测到物件机率(信心)的 list,例如:
    [0.8760120272636414, 0.6513471007347107]

另外 detect_common_objects 还有一个很重要的输入参数为信心阈值 confidence,可以看到上方模型的输出向量包含了图片中不同物件是否存在的机率 (conf),confidence 就是用来控制机率大於多少才判定为存在,其预设值为 0.5,调整此输入参数的效果如下:
confidence test
可以看到信心阈值为 0.5 时,模型少侦测了许多人、车,而降低信心阈值的确可以让模型成功侦测到大部分的人、车,但为了正确侦测到图片中的物件,我们需要将信心阈值设得很低。

在提高、降低这类参数时都要格外小心,因为改变它们的值有可能产生非预期的结果。

为何使用 fastAPI?

FastAPI 让我们可以轻松建立网页服务器来 host 模型,而且除了速度很快以外,它还内建了能与服务器互动的图形化介面客户端,只需要访问 /docs 接口即可 (例如 http://localhost:8000/docs)。

使用 fastAPI 部署模型

概念简介

在对模型运作有基本的概念之後,我们准备进入重头戏了,但在开始部署之前,先简单复习一些重要的概念,以及 fastAPI 如何实现这些概念。

主从式架构 (Client-Server model)

通常讲到部署,实际上指的是把进行预测所需的全部软件放进一个 服务器(server) 中,如此一来,客户端(client) 可以藉由将 请求(requests) 传送到服务器来与机器学习模型互动。
模型会在服务器中等待客户端发送的预测请求,因此请求必须包含模型进行预测所需的资讯,服务器则会使用这些资讯来将预测回传至客户端。

通常一次 request 会包含数个预测需求。

首先,我们可以建立 FastAPI class 的实例 (instance):

app = FastAPI()

接着使用这个实例来建立负责处理预测之逻辑的接口 (endpoints),当所有程序码都就定位之後,只需要执行以下指令就能启动服务器:

uvicorn.run(app)

我们的 API 是使用 fastAPI 编写的,但上线 (serving) 是使用 uvicorn,它是速度非常快的非同步服务器闸道界面 (Asynchronous Server Gateway Interface, ASGI),这两样技术会密切连接,但我们并不需要了解实作的细节,现在只需要知道 uvicorn 负责 serving 就够了。

接口 (Endpoints)

同一个服务器可以 host 多个模型,只须为它们指派各自的接口即可随意选用,而每个接口都是由 URL 的型式来代表。 假设我们有一个网站叫 myawesomemodel.com,若上面有三个不同的模型,它们可分别位於以下接口:

  • myawesomemodel.com/count-cars/
  • myawesomemodel.com/count-apples/
  • myawesomemodel.com/count-plants/

而每个模型的功能就如其接口命名型式所示。
在 FastAPI 中,定义接口的步骤如下:

  1. 撰写一个处理该接口所有运作逻辑的函式。
  2. 使用包含 HTTP method 与 URL 型式的函式 装饰(decorating) 它。

以下示范允许 HTTP GET 请求的接口 "/my-endpoint"

@app.get("/my-endpoint")
def handle_endpoint():
    ...
    ...

HTTP 请求 (HTTP Requests)

客户端与服务器藉由 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 函式会包含参数,因为它预期客户提供某些资讯。
好啦,以上就是所有会用的基本知识,开始进入正题吧!!

撰写 API

首先引入必要的函式:

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/ 看看它是否成功运作,若成功运作应该可以看到以下画面:
check api

藉由访问 http://localhost:8000/docs 可以进入 fastAPI 提供的内建 client,试试上传图片来看看我们的 API 如何侦测图中的物件并回传加上定界框与类别的图片吧。
其介面如下,点击 /predict 接口可以开启更多选项,接着点击 Try it out 就可以开始测试 API 了:
main

点开後可以在 model 栏位选取要使用的模型 (注意若选用 YOLO 模型需要等待一段时间让权重下载完)。
点击 file 栏位的 选择档案 则可以上传想要进行辨识的图片,最後点击蓝色 Execute 即可发送 HTTP 请求至服务器,最後往下滑就可以看到辨识的结果了。
model

可以试试各种不同的图片与信心阈值,当模型漏掉某些物件时可以试着降低阈值。

由另一个 Client 使用模型

fastAPI 让我们可以使用内建的 client 与 API 互动固然很棒,但其实我们也可以不使用 UI,直接用程序码与其互动。
明天我们就会撰写一个简单的 client,然後使用它来与 API 互动,明天见啦。
/images/emoticon/emoticon33.gif

如果等不及看完明天的内容,请保持服务器执行,然後在另一个分页打开 client.ipynb 笔记本吧。

参考资料


<<:  Day 07: Creational patterns - Abstract Factory

>>:  day8 储存设备 (雷)大家都不一样呢

undefined 、 undeclared 、 null 的区别

这几天忙着北上,今天分享比较简单的内容,关於「undefined 、 undeclared 、 nu...

React学习动机&前端框架简单介绍

学习动机 过去开发网页前端程序的时候,使用基本的html、css、javascript作为开发的工具...

Vue.js 从零开始:元件

Vue 元件概念 Component元件,作为SPA的灵魂功能,可以将程序码以及模组封装起来,增加程...

Day 18 — To Do List (5) 新增 To Do Event

昨天我们快乐 (?) 的把资料 render 到网页上(虽然会有点 Delay,对 UX 不好…不过...

【少女人妻的30天Elastic】Day 27 : App Search_API 介绍与应用_Search Settings

Aloha!又是我少女人妻 Uerica!这个连假又过了一场奇幻旅程,交了两个好朋友,心得是原来我...