-
Notifications
You must be signed in to change notification settings - Fork 344
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
6th/fastapi code with comment (#187)
- Loading branch information
Showing
8 changed files
with
187 additions
and
99 deletions.
There are no files selected for viewing
140 changes: 96 additions & 44 deletions
140
02-online-serving(fastapi)/projects/web_single/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,73 +1,125 @@ | ||
# Web Single Pattern ML API by FastAPI | ||
|
||
Web Single Pattern ML API by FastAPI는 FastAPI를 이용해 만든 웹 서비스로, 단일 모델을 이용해 예측을 수행하는 API를 제공합니다. | ||
|
||
## Pre-requisites | ||
# FastAPI Web Single Pattern | ||
- 목적 : FastAPI를 사용해 Web Single 패턴을 구현합니다 | ||
- 상황 : 데이터 과학자가 model.py을 만들었고(model.joblib이 학습 결과), 그 model을 FastAPI을 사용해 Online Serving을 구현해야 함 | ||
- model.py는 추후에 수정될 수 있으므로, model.py를 수정하지 않음(데이터 과학자쪽에서 수정) | ||
|
||
## 설치 | ||
- Python >= 3.9 | ||
- Poetry >= 1.1.4 | ||
|
||
## Installation | ||
|
||
```bash | ||
``` | ||
poetry install | ||
``` | ||
|
||
## Run | ||
|
||
```bash | ||
PYTHONPATH=. | ||
poetry run python main.py | ||
python main.py | ||
``` | ||
|
||
## Usage | ||
|
||
### Predict | ||
|
||
## Predict | ||
```bash | ||
curl -X POST "http://0.0.0.0:8000/predict" -H "Content-Type: application/json" -d '{"features": [5.1, 3.5, 1.4, 0.2]}' | ||
|
||
{"id":3,"result":0} | ||
``` | ||
|
||
### Get all predictions | ||
|
||
## Get all predictions | ||
```bash | ||
curl "http://0.0.0.0:8000/predict" | ||
|
||
[{"id":1,"result":0},{"id":2,"result":0},{"id":3,"result":0}] | ||
``` | ||
|
||
### Get a prediction | ||
|
||
## Get a prediction | ||
```bash | ||
curl "http://0.0.0.0:8000/predict/1" | ||
{"id":1,"result":0} | ||
``` | ||
|
||
## Build | ||
|
||
```bash | ||
## Docker Build | ||
``` | ||
docker build -t web_single_example . | ||
``` | ||
|
||
## Project Structure | ||
--- | ||
|
||
|
||
|
||
# 시작하기 전에 | ||
- 여러분들이라면 어떻게 시작할까? 어떻게 설계할까? => 잠깐이라도 생각해보기 | ||
- 여러분들의 생각과 제가 말하는 것을 비교 => Diff => 이 Diff가 왜 생겼는가? | ||
|
||
# FastAPI 개발 | ||
- FastAPI를 개발할 때의 흐름 | ||
- 전체 구조를 생각 => 파일, 폴더 구조를 어떻게 할까? | ||
- predict.py, api.py, config.py | ||
- 계층화 아키텍처 : 3 tier, 4 tier layer | ||
- Presentation(API) <-> Application(Service) <-> Database | ||
- API : 외부와 통신. 건물의 문처럼 외부 경로. 클라이언트에서 API 호출. 학습 결과 Return | ||
- schema : FastAPI에서 사용되는 개념 | ||
- 자바의 DTO(Data Transfer Object)와 비슷한 개념. 네트워크를 통해 데이터를 주고 받을 때, 어떤 형태로 주고 받을지 정의 | ||
- 예측(Request, Response) | ||
- Pydantic의 Basemodel을 사용해서 정의. Request, Response에 맞게 정의. Payload | ||
- Application : 실제 로직. 머신러닝 모델이(딥러닝 모델이) 예측/추론. | ||
- Database : 데이터를 어딘가 저장하고, 데이터를 가지고 오면서 활용 | ||
- Config : 프로젝트의 설정 파일(Config)을 저장 | ||
- 역순으로 개발 | ||
|
||
# 구현해야 하는 기능 | ||
## TODO Tree 소개 | ||
- TODO Tree 확장 프로그램 설치 | ||
- [ ] : 해야할 것 | ||
- [x] : 완료 | ||
- FIXME : FIXME | ||
|
||
# 기능 구현 | ||
- [x] : FastAPI 서버 만들기 | ||
- [x] : POST /predict : 예측을 진행한 후(PredictionRequest), PredictionResponse 반환 | ||
- [x] : Response를 저장. CSV, JSON. 데이터베이스에 저장(SQLModel) | ||
- [x] : GET /predict : 데이터베이스에 저장된 모든 PredictionResponse를 반환 | ||
- [x] : GET /predict/{id} : id로 필터링해서, 해당 id에 맞는 PredictionResponse를 반환 | ||
- [x] : FastAPI가 띄워질 때, Model Load => lifespan | ||
- [x] : DB 객체 만들기 | ||
- [x] : Config 설정 | ||
|
||
# 참고 | ||
- 데이터베이스는 SQLite3, 라이브러리는 SQLModel을 사용 | ||
|
||
# SQLModel | ||
- FastAPI를 만든 사람이 만든 Python ORM(Object Relational Mapping) : 객체 => Database | ||
- 데이터베이스 = 테이블에 데이터를 저장하고 불러올 수 있음 | ||
- Session : 데이터베이스의 연결을 관리하는 방식 | ||
- 외식. 음식점에 가서 나올 때까지를 하나의 Session으로 표현. Session 안에서 가게 입장, 주문, 식사 | ||
- Session 내에서 데이터를 추가, 조회, 수정할 수 있다! => POST / GET / PATCH | ||
- Transaction : 세션 내에 일어나는 모든 활동. 트랜잭션이 완료되면 결과가 데이터베이스에 저장됨 | ||
|
||
## 코드 예시 | ||
``` | ||
SQLModel.metadata.create_all(engine) : SQLModel로 정의된 모델(테이블)을 데이터베이스에 생성 | ||
- 처음에 init할 때 테이블을 생성! | ||
``` | ||
|
||
```bash | ||
. | ||
├── .dockerignore # 도커 이미지 빌드 시 제외할 파일 목록 | ||
├── .gitignore # git에서 제외할 파일 목록 | ||
├── Dockerfile # 도커 이미지 빌드 설정 파일 | ||
├── README.md # 프로젝트 설명 파일 | ||
├── __init__.py | ||
├── api.py # API 엔드포인트 정의 파일 | ||
├── config.py # Config 정의 파일 | ||
├── database.py # 데이터베이스 연결 파일 | ||
├── db.sqlite3 # SQLite3 데이터베이스 파일 | ||
├── dependencies.py # 앱 의존성 관련 로직 파일 | ||
├── main.py # 앱 실행 파일 | ||
├── model.joblib # 학습된 모델 파일 | ||
├── model.py # 모델 관련 로직 파일 | ||
├── poetry.lock # Poetry 라이브러리 버전 관리 파일 | ||
└── pyproject.toml # Poetry 프로젝트 설정 파일 | ||
``` | ||
``` | ||
with Session(engine) as session: | ||
... | ||
# 테이블에 어떤 데이터를 추가하고 싶은 경우 | ||
result = '' | ||
session.add(result) # 새로운 객체를 세션에 추가. 아직 DB엔 저장되지 않았음 | ||
session.commit() # 세션의 변경 사항을 DB에 저장 | ||
session.refresh(result) # 세션에 있는 객체를 업데이트 | ||
# 테이블에서 id를 기준으로 가져오고 싶다 | ||
session.get(DB Model, id) | ||
# 테이블에서 쿼리를 하고 싶다면 | ||
session.query(DB Model).all() # 모든 값을 가져오겠다 | ||
``` | ||
|
||
# SQLite3 | ||
- 가볍게 사용할 수 있는 데이터베이스. 프러덕션 용도가 아닌 학습용 | ||
|
||
|
||
# 현업에서 더 고려해야 하는 부분 | ||
- Dev, Prod 구분에 따라 어떻게 구현할 것인가? | ||
- Data Input / Output 고려 | ||
- Database => Cloud Database(AWS Aurora, GCP Cloud SQL) | ||
- API 서버 모니터링 | ||
- API 부하 테스트 | ||
- Test Code |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
from pydantic import Field | ||
from pydantic_settings import BaseSettings | ||
|
||
|
||
class Config(BaseSettings): | ||
db_url: str = Field(default="sqlite:///./db.sqlite3", env="DB_URL") | ||
model_path: str = Field(default="model.joblib", env="MODEL_PATH") | ||
app_env: str = Field(default="local", env="APP_ENV") | ||
|
||
|
||
config = Config() | ||
config = Config() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,12 @@ | ||
import datetime | ||
from typing import Optional | ||
|
||
from sqlmodel import Field, SQLModel, create_engine | ||
|
||
from sqlmodel import SQLModel, Field, create_engine | ||
from config import config | ||
|
||
|
||
class PredictionResult(SQLModel, table=True): | ||
id: Optional[int] = Field(default=None, primary_key=True) | ||
class PredictionResult(SQLModel,table=True): | ||
id: int = Field(default=None, primary_key=True) | ||
result: int | ||
created_at: Optional[str] = Field(default_factory=datetime.datetime.now) | ||
|
||
created_at: str = Field(default_factory=datetime.datetime.now) | ||
# default_factory : default를 설정. 동적으로 값을 지정. | ||
|
||
engine = create_engine(config.db_url) | ||
engine = create_engine(config.db_url) |
6 changes: 2 additions & 4 deletions
6
02-online-serving(fastapi)/projects/web_single/dependencies.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,11 @@ | ||
model = None | ||
|
||
|
||
def load_model(model_path: str): | ||
import joblib | ||
|
||
global model | ||
model = joblib.load(model_path) | ||
|
||
|
||
def get_model(): | ||
global model | ||
return model | ||
return model |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,34 @@ | ||
from contextlib import asynccontextmanager | ||
|
||
from fastapi import FastAPI | ||
from contextlib import asynccontextmanager | ||
from loguru import logger | ||
from sqlmodel import SQLModel | ||
|
||
from api import router | ||
from config import config | ||
from database import engine | ||
from dependencies import load_model | ||
|
||
from api import router | ||
|
||
@asynccontextmanager | ||
async def lifespan(app: FastAPI): | ||
# 데이터베이스 테이블 생성 | ||
logger.info("Creating database tables") | ||
logger.info("Creating database table") | ||
SQLModel.metadata.create_all(engine) | ||
|
||
# 모델 로드 | ||
logger.info("Loading model") | ||
load_model(config.model_path) | ||
|
||
# model.py에 존재. 역할을 분리해야 할 수도 있음 => 새로운 파일을 만들고, 거기서 load_model 구현 | ||
yield | ||
|
||
|
||
app = FastAPI(lifespan=lifespan) | ||
app.include_router(router) | ||
|
||
@app.get("/") | ||
def root(): | ||
return "Hello World!" | ||
|
||
|
||
if __name__ == "__main__": | ||
import uvicorn | ||
|
||
uvicorn.run(app, host="0.0.0.0", port=8000) | ||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) |
40 changes: 40 additions & 0 deletions
40
02-online-serving(fastapi)/projects/web_single/requirements.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
annotated-types==0.6.0 | ||
anyio==3.7.1 | ||
certifi==2023.11.17 | ||
click==8.1.7 | ||
dnspython==2.4.2 | ||
email-validator==2.1.0.post1 | ||
exceptiongroup==1.2.0 | ||
fastapi==0.105.0 | ||
h11==0.14.0 | ||
httpcore==1.0.2 | ||
httptools==0.6.1 | ||
httpx==0.25.2 | ||
idna==3.6 | ||
itsdangerous==2.1.2 | ||
Jinja2==3.1.2 | ||
joblib==1.3.2 | ||
loguru==0.7.2 | ||
MarkupSafe==2.1.3 | ||
numpy==1.26.2 | ||
orjson==3.9.10 | ||
pydantic==2.5.2 | ||
pydantic-extra-types==2.2.0 | ||
pydantic-settings==2.1.0 | ||
pydantic_core==2.14.5 | ||
python-dotenv==1.0.0 | ||
python-multipart==0.0.6 | ||
PyYAML==6.0.1 | ||
scikit-learn==1.3.2 | ||
scipy==1.11.4 | ||
sniffio==1.3.0 | ||
SQLAlchemy==2.0.23 | ||
sqlmodel==0.0.14 | ||
starlette==0.27.0 | ||
threadpoolctl==3.2.0 | ||
typing_extensions==4.9.0 | ||
ujson==5.9.0 | ||
uvicorn==0.24.0.post1 | ||
uvloop==0.19.0 | ||
watchfiles==0.21.0 | ||
websockets==12.0 |
Oops, something went wrong.