papermaill을 사용하여 ipynb에서 웹 서버 구축

TL;DR


기계 학습에 관한 시스템 개발은jupyter에서 모델을 개발한 후 활용할 때python 파일로 바꾸는 상황을 볼 수 있다.
이번에는 다시 쓰지 않아도 활용할 수 있도록 ipynb 파일에 공유해 웹 서버를 구축하는 방법입니다.
ipynb 파일의 실행은papermaill이라는 프로그램 라이브러리를 사용합니다.
https://papermill.readthedocs.io/en/latest/
또한 이 문서에 기재된 모든 코드는 아래 창고에 저장됩니다.
https://github.com/uniocto/prediction-server-with-nb

단계

  • 테스트용 모델 구축
  • 예측용 notebook 제작
  • 서버용 docker 이미지 제작
  • 동작 확인
  • 느낌
  • 최종 파일 구성


    최종적으로 다음 파일 구조를 만듭니다.
     ./  
     ├── model  # ステップ1で作成
     ├── requirements.txt  # ステップ2で作成
     ├── main.ipynb  # ステップ2で作成
     ├── docker-compose.yml  # ステップ3で作成
     └── dockerfile  # ステップ3で作成
    

    1. 테스트용 모델 구축


    아이리스 데이터 세트를 사용하여 간단한 SVC 모델을 만듭니다.
    import pickle
    import numpy as np
    from sklearn.svm import SVC
    from sklearn.datasets import load_iris
    from sklearn.model_selection import train_test_split
    from sklearn.metrics import accuracy_score
    
    iris_dataset = load_iris()
    
    x = iris_dataset["data"]
    y = iris_dataset["target"]
    
    
    x_train, x_test, y_train, y_test = train_test_split(
        x,
        y,
        test_size = 0.2,
        train_size = 0.8,
        shuffle = True)
    
    
    clf = SVC(gamma='scale')
    clf.fit(x_train, y_train)
    
    y_pred = clf.predict(x_test)
    print(accuracy_score(y_test, y_pred))
    
    filename = 'model'
    pickle.dump(clf, open(filename, 'wb'))
    
    clf = pickle.load(open(filename, 'rb'))
    y_pred = clf.predict(x_test)
    print(accuracy_score(y_test, y_pred))
    

    2. 노트북으로 제작 예측


    papermaill에서 호출된 Requirements.txt 파일과 ipynb 파일을 만듭니다.python 서버라면
    WSGI와 ASGI가 있어 참고로 양측을 준비했다.
    또한 이번 WSGI는 wsgiref를, ASGI는 uvicorn을 사용했으니 좋아하는 사람을 이용할 수 있을 것 같다.

    WSGI의 경우


    # requirements.txt
    sklearn
    numpy
    papermill
    
    # main.ipynb
    import json
    import pickle
    import numpy as np
    from datetime import datetime
    from wsgiref.util import setup_testing_defaults
    from wsgiref.simple_server import make_server
    
    PORT = 8000
    
    class ModelPredictor:
        
        def __init__(self):
            self.model = pickle.load(open('model', 'rb'))
    
        def main(self,data):
            return json.dumps({"val": self.predict(self.prep(data))} ,cls = NumpyEncoder).encode()    
    
        def prep(self,data):
            return [[   data['sepal length (cm)'],
                        data['sepal width (cm)'],
                        data['petal length (cm)'],
                        data['petal width (cm)']]]
    
        def predict(self,data):
            return self.model.predict(data)[0]
    
    class NumpyEncoder(json.JSONEncoder):
    
        def default(self, obj):
            if isinstance(obj, np.integer):
                return int(obj)
            elif isinstance(obj, np.floating):
                return float(obj)
            elif isinstance(obj, np.ndarray):
                return obj.tolist()
            else:
                return super(NumpyEncoder, self).default(obj)
    
    prd_controller = ModelPredictor()
    
    prd_controller.predict(
        {"sepal length (cm)": 1,
        "sepal width (cm)": 1,
        "petal length (cm)": 1,
        "petal width (cm)":1})
    
    def simple_app(environ, start_response):
        setup_testing_defaults(environ)
        
        wsgi_input = environ["wsgi.input"]
        content_length = int(environ["CONTENT_LENGTH"])
        data = json.loads(wsgi_input.read(content_length))
    
        print(wsgi_input)
        print(data)
    
        status = '200 OK'
        headers = [('Content-type', 'text/plain; charset=utf-8')]
        ret = [prd_controller.main(data)]
        start_response(status, headers)
    
        return ret
    
    with make_server('0.0.0.0', PORT, simple_app) as httpd:
        print(f"Serving on port {PORT}...")
        httpd.serve_forever()
    

    ASGI의 경우


    # requirements.txt
    sklearn
    numpy
    papermill
    uvicorn
    
    # main.ipynb
    import json
    import pickle
    import uvicorn
    import numpy as np
    import nest_asyncio
    from datetime import datetime
    
    nest_asyncio.apply()
    
    PORT = 8000
    
    class ModelPredictor:
        
        def __init__(self):
            self.model = pickle.load(open('model', 'rb'))
    
        def predict(self,body):
            rt_val = self.model.predict([[
                        body['sepal length (cm)'],
                        body['sepal width (cm)'],
                        body['petal length (cm)'],
                        body['petal width (cm)']]])
            return json.dumps({"val": rt_val[0]}, cls=NumpyEncoder).encode()
    
    class NumpyEncoder(json.JSONEncoder):
    
        def default(self, obj):
            if isinstance(obj, np.integer):
                return int(obj)
            elif isinstance(obj, np.floating):
                return float(obj)
            elif isinstance(obj, np.ndarray):
                return obj.tolist()
            else:
                return super(NumpyEncoder, self).default(obj)
    
    prd_controller = ModelPredictor()
    
    prd_controller.predict(
        {"sepal length (cm)": 1,
        "sepal width (cm)": 1,
        "petal length (cm)": 1,
        "petal width (cm)":1})
    
    async def read_body(receive):
        body = b''
        more_body = True
    
        while more_body:
            message = await receive()
            body += message.get('body', b'')
            more_body = message.get('more_body', False)
    
        return json.loads(body)
    
    
    async def app(scope, receive, send):
        body = await read_body(receive)
        ret = prd_controller.predict(body)
        await send({
            'type': 'http.response.start',
            'status': 200,
            'headers': [
                [b'content-type', b'text/plain'],
            ]
        })
        await send({
            'type': 'http.response.body',
            'body': ret,
        })
    
    uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")
    

    3. 서버용 docker 이미지 만들기


    예측 웹 서버의 docker 이미지를 만들기 위해 dockerfile을 기술합니다.
    mainpapermill main.ipynb out.ipynb에서ipynb 파일을 실행한 결과out입니다.ipynb로 출력합니다.
    이번에는 컨테이너의 운행에 docker-compose, docker-compose를 사용하기 위해서입니다.제작도 가능합니다.
    # dockerfile
    FROM jupyter/datascience-notebook:latest
    WORKDIR /home/jovyan
    COPY model ./
    COPY main.ipynb ./
    COPY requirements.txt ./
    RUN pip install -r requirements.txt
    ENTRYPOINT [ "papermill","main.ipynb","out.ipynb"] 
    
    # docker-compose.yml
    version: '3'
    services:
      notebook_server:
        build: .
        container_name: notebook_server
        hostname: notebook_server
        restart: always
        ports: 
          - 8000:8000
    

    4. 동작 확인


    다음 명령을 실행하여 구축 후curl을 통해 동작을 확인합니다.{"val": INTEGER} 형식으로 데이터를 반환하면 성공합니다.
    $ docker-compose up -d
    $ curl -X POST -H "Content-Type: application/json" -d '{  "sepal length (cm)": 1,  "sepal width (cm)": 1,  "petal length (cm)": 1,  "petal width (cm)":1}' localhost:8000
    {"val": 0}
    

    감상


    소개는 했지만 다음과 같은 몇 가지 점에서 추천하지 않습니다.
  • papermill은 대량 용도에 적합한 도구로 웹 서버에 적합하지 않다
  • 이미지 사이즈는 이번에 3.7GB 정도
  • 자원이 풍부하고 데이터 과학의 생산성을 극대화하려면 후보가 될 수 있다.

    좋은 웹페이지 즐겨찾기