Python
Docker기반의 RESTful api로 pytorch model 배포하기[1-5]
DoDo&ToTo
2020. 11. 19. 16:18
백엔드에 대해서는 따로 배운적은 없지만..
예전부터 model을 만드는 것부터 배포하는 것까지의 과정을 한번 해보고 싶긴 했었다.
해당 과정에 대한 기록을 남겨두기 위해서 오랜만에 블로그를 열었다 :)
pytorch로 model 학습을 완료한 상태라고 가정을 하고, 해당 model을 restful 형태로 service하는 과정을 남겨본다.
대략적인 순서는 아래와 같음.
- Flask RESTful을 이용하여, HTTP로 호출할 수 있는 RESTful api 만들기
- 학습한 모델의 weight와 graph를 하나의 파일로 만들기(torchscript)
- 보안을 위해, 2번에서 만든 torchscript파일을 encryption하기
- 보안을 위해, cython을 이용하여 python script를 library형태로 만들기
- Docker image 만들고 배포하기
편의를 위해서, 하나의 순서 당 하나의 글을 쓸 예정
이번 글에서는, 1번에 대해 정리해본다.
- Flask는 python library로 웹 개발을 손쉽게 해주는 tool이다.
- 내부의 자세한 구조나 형태는 웹 개발을 안해봐서 아직 지식이 부족하다.
- Flask를 기반으로 RESTful api를 더 손쉽게 만들 수 있도록 해주는 tool로 Flask-RESTful이 있다.
- pip를 이용해서 간단하게 설치 가능
pip install Flask-RESTful
- 자, 이제 간단한 RESTful web server를 10줄 이내의 코드로 만들 수 있다.
import argparse
from flask import Flask
from flask_restful import Api
from resources.MyAPI import MyAPI
from resources.custom_errors import errors
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--port', type=int, required=True, help='port number')
parser.add_argument('--debug', default=False, action='store_true', help='debug mode')
parser.add_argument('--thread', default=False, action='store_true', help='multi-thread mode')
parser.add_argument('--processes', type=int, default=1, help='num. of processes')
opt = parser.parse_args()
app = Flask(__name__)
api = Api(app, errors=errors)
api.add_resource(MyAPI, '/predict', resource_class_kwargs={'debug': opt.debug})
app.run(port=opt.port, debug=opt.debug, threaded=opt.thread, processes=opt.processes)
- web server 완성! args 관련 부분을 제외하면 4줄이면 web server를 만들 수 있다.
- Flask를 초기화하고, 이를 restful api로 감싸준다.
- restful api를 이용해서 resource를 추가할 수 있다. 여기서 resource는 url 하나로 이해하면 쉽다.
- 하나의 resource의 복수의 url을 지정할 수도 있다.
- 즉, server_address/predict를 web에서 호출하면 MyAPI에 정의된 method가 실행되도록 routing 해준다. (편하다..)
- resource에 등록하는 것은 Resource class를 상속한 class로 정의된다.
- class의 init function에 넘겨줄 argument는 resource_class_kwargs에 dictionary 형태로 넘겨줄 수 있다.
- api에 resource를 추가하고 나면, app.run으로 server를 올릴 수 있다. 이 때 port 번호나 debuging mode 등을 parameter로 줄 수 있다.
from flask_restful import Resource, reqparse
import werkzeug
import numpy as np
import cv2
from resources.MyModel import MyModel
from resources.custom_errors import WrongMethodError, NoneArgumentError, NotExistResultError
model = MyModel()
class MyAPI(Resource):
def __init__(self, **kwargs):
# Create a request parser
parser = reqparse.RequestParser()
parser.add_argument('image', dest='image', type=werkzeug.datastructures.FileStorage, location='files')
self.args = parser.parse_args(strict=True)
self.debug = kwargs['debug']
def get(self):
raise WrongMethodError
def delete(self):
raise WrongMethodError
def put(self):
raise WrongMethodError
def post(self):
''' prediction '''
image_file = self.args.get('image', None)
if image_file is None:
raise NoneArgumentError
image_bytes = image_file.read()
image = cv2.imdecode(np.frombuffer(image_bytes, dtype=np.uint8), cv2.IMREAD_COLOR)
preds = model.prediction(image=image, debug=self.debug)
return preds
- 앞서 작성한 server에서 사용할 resource에 대한 간단한 예제이다.
- 앞서 말한 것과 같이 Resource class를 상속하였다.
- 그리고 HTTP method에서 image file을 argument로 받아야 하는데, 이 부분도 RequestParser를 이용하면 간단하게 처리가 가능하다. (놀랍다..)
- dest는 해당 argument를 parsing했을 때 dictionary에 저장될 key name이며, type은 file의 type을 의미한다.
- location은 HTTP packet에서 data의 위치를 의미하며, image는 file로 넘겨줄 것이므로 files가 된다. file이 아닐 경우 form, data, header 등등 원하는 위치로 정하면 된다.
- HTTP method인 GET, PUT, DELETE, POST에 맞게 class 함수를 정의해주면 된다. 함수명은 위와 같이 소문자로 정하면 별도의 decorator가 없어도 알아서 routing이 된다.
- 위의 예제에서는 사용하지 않는 method의 함수도 다 정의를 했지만, 꼭 재정의할 필요는 없다. 안하면 알아서 exception 처리해준다.
- 우리는 image를 POST method로 받아서 결과를 return할 것이므로, post 함수를 위와 같이 간단하게 정의할 수 있다.
- argument를 parsing해서 image의 binary값을 얻고, 이를 decoding 시켜서 image형태로 만들어 준다.
- 그리고 image를 model에 넘겨줘서 prediction하고 결과를 return한다.
다음 번에는 위에서 나온 model을 torchscript와 file encryption을 이용해서 어떻게 저장하고 불러 올지에 대해 쓸 예정이다.