관리 메뉴

나는 문어~ 꿈을 꾸는 문어~

머신러닝 모델을 서빙해보자 (ec2 + sklearn + flask + docker) 본문

MLOPS & 백엔드

머신러닝 모델을 서빙해보자 (ec2 + sklearn + flask + docker)

harrykur139 2022. 8. 11. 12:14

머신러닝을 서비스에서 사용하기 위해서는 모델링과 서빙이 필요하다. 누군가 성능이 아주 좋은 모델을 만들었다고 해도 이를 서비스하기 위해서는 많은 고민이 필요하다. 모델을 어떤 서버에 올릴 것인지, 시간이 지남에 따라 모델의 성능을 어떻게 모니터링 할 것인지, 특정 기간마다 어떻게 자동으로 재학습을 시킬 것인지 등이다. 이러한 것을 다루는 분야를 머신러닝 엔지니어링 혹은 MLOPS라고 부르는 것 같다.

 

이번 포스팅은 머신러닝 모델 서빙을 가장 간단한 수준으로 하여 첫 걸음을 떼보려한다.

 

전체 코드 링크는 다음과 같다.

https://github.com/euiraekim/flask-model-serving

순서

  1. sklearn으로 모델 학습 후 저장
    • 어떤 모델을 학습시키냐보다 서빙에 중점을 두었기 때문에 iris 데이터셋을 학습했다.
  2. flask로 모델 서빙 코드 구현 후 localhost로 접속 테스트
  3. aws ec2에 코드 올린 후 외부 접속 테스트
  4. docker 이미지를 만들고 컨테이너 구동 후 외부 접속 테스트
    • wsgi 프로그램인 gunicorn 사용했다. 일반적으로 맨 앞에 nginx를 두지만 이번 포스팅에서는 사용하지 않는다.

 

 

Iris 모델 학습 / 저장

# model/train.py

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
import joblib

iris = datasets.load_iris()
x = iris.data
y = iris.target
print(x,y)
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size = 0.2)

pipeline = Pipeline([('std', StandardScaler()), ('lr', LogisticRegression())])
pipeline.fit(x_train, y_train)

pred = pipeline.predict(x_test)

print(f'Accuracy : {accuracy_score(y_test, pred)}')

file_name = 'model.pkl'
joblib.dump(pipeline, file_name)
print('Model saved.')

가장 간단한 형태의 모델 학습 코드다. 로지스틱 회귀 모델을 서빙하기 위해서는 학습 때 사용한 StandardScaler를 사용해야하기 때문에 이 둘을 pipeline으로 묶어서 학습하고 저장했다.

 

위 코드를 실행하면 학습된 모델이 model/model.pkl 파일로 저장된다. 이 모델을 서빙에 사용할 것이다.

 

 

Flask 모델 서빙 구현

# app.py

from flask import Flask, request

import numpy as np
import joblib

app = Flask(__name__)

model_file = 'model/model.pkl'
model = joblib.load(model_file)

COLUMNS = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
CLASSES = ['setosa', 'versicolor', 'virginica']

@app.route('/health', methods=['GET'])
def health():
    return {
        'status': 200,
    }

@app.route('/predict', methods=['GET'])
def predict():
    feature_dict = request.get_json()
    x = [feature_dict[col] for col in COLUMNS]
    x = np.array(x).reshape(1, -1)
    pred = model.predict(x)[0]

    return {
        'status': 200,
        'class': CLASSES[pred]
    }

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True)

간단한 코드이므로 따로 파일을 나누지 않고 app.py 한 파일에 작성했다. 위처럼 짧은 코드로도 health check 기능과 모델 predict 기능 가진 서버를 구현할 수 있다.

 

 

아래 명령어로 flask 서버를 로컬에 구동시켜보자.

flask run -p 5000

 

먼저 health check 엔드포인트에 요청을 날려보자. 나는 Postman을 사용했다.

localhost의 5000번 포트로 요청을 날린 결과 status 200을 반환하며 잘 작동하는 것을 알 수 있다.

 

 

이번에는 실제로 predict 요청을 해보자. 학습에 사용한 4개의 feature를 json 형식으로 body에 담아 보냈다.

class가 virginica라고 올바르게 응답이 오는 것을 확인할 수 있다.

 

 

AWS ec2에 flask 서버 올리기

로컬에서 flask를 구동시킨 것은 테스트를 하기 위함이었다. 실제로 서비스를 하려면 외부 접속을 가능하게 해야한다. 생각나는 방법은 두 가지인데 첫 번째는 로컬에 서버를 구동하고 포트포워딩을 하여 외부 접속을 가능케하는 것이고, 두 번째는 클라우드를 사용하는 것이다. 나는 AWS의 ec2 서비스를 사용하여 인스턴스를 만들고 서빙을 할 것이다.

 

먼저 AWS ec2 인스턴스를 실행하고 보안그룹 인바운드 규칙에서 5000번 포트를 열어두자. 그리고 구현한 코드를 github에 올려두자. 이 과정은 여기서 다루지 않겠다.

 

깃허브에서 코드를 내려받고 패키지들을 설치하자.

https://github.com/euiraekim/flask-model-serving.git
cd flask-model-serving
pip install -r requirements.txt

 

flask 서버를 실행하자. 모든 외부 접속을 열기 위해 -h (--host) 옵션에 0.0.0.0을 넣어주어야 한다.

flask run -p 5000 -h 0.0.0.0

 

postman으로 요청이 정상적으로 돌아오는지 확인해보자. 바꾼 것은 localhost를 aws의 주소로 바꾼 것 뿐이다.

 

 

 

Docker 컨테이너로 서빙하기

지금까지는 flask run 명령어를 통해 서버를 실행했다. 이때 실행되는 서버는 개발용 서버다. production에 대한 고려없이 만들어진 서버라 불안정하다. 실제로 이렇게 서버를 실행했을 때 flask에서 warning 메시지를 띄운다.

 

일반적으로 production할 때는 flask 앞에 WSGI라는 프로그램을 둔다. 이는 웹서버가 파이썬 스크립트와 잘 통신할 수 있도록 해주는 프로그램이다.

보통 이 앞에 nginx같은 웹서버를 두어 client-nginx-wsgi-flask와 같이 통신하도록 하지만 여기서 nginx는 다루지 않는다. 그리고 wsgi는 가장 많이쓰이는 uwsgi와 gunicorn 중 gunicorn을 사용했다.

 

먼저 프로젝트 루트 폴더 아래에 Dockerfile을 작성하자.

FROM python:3.9

WORKDIR /app

COPY . /app

RUN pip install -r requirements.txt

EXPOSE 5000
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:5000", "-w", "2","--timeout", "120"]
  • FROM : base image
  • WORKDIR : 작업할 폴더
  • COPY : host 컴퓨터의 현재 디렉토리의 파일들을 container의 /app에 복사
  • RUN : 명령어 실행
  • EXPOSE : 외부에 공개할 포트 명시
  • CMD : 컨테이너 실행 후 명령어

 

도커 이미지를 빌드하자.

sudo docker build -t flask-server .

 

도커 컨테이너를 실행하자. -p 옵션을 줘서 host 5000번 포트로 들어오는 요청을 컨테이너의 5000번 포트로 매핑한다.

sudo docker run -p 5000:5000 flask-server

 

이제 동일하게 postman으로 요청을 보내보면 정상적으로 응답하는 것을 확인할 수 있다.

Comments