나는 보통 개발 프로젝트를 진행할 때 서버로 NodeJS를 사용하는데, 머신러닝 관련 라이브러리는 파이썬이 훨씬 잘되어있는 것 같다. Show 그래서 백단 서버는 NodeJS로 운영하되, client에서 머신러닝 모델을 돌려야 하는 요청이 오면 별도 파이썬 서버로 넘긴 후 결과를 NodeJS로 받아내는 방법이 없을까 고민했다. stackoverflow.com/questions/59738972/can-i-use-node-js-for-the-back-end-and-python-for-the-ai-calculations Can I use Node.js for the back end and Python for the AI calculations? I am trying to create a website in Node.js. Though, as I am taking a course on how to use Artificial Intelligence and would like to implement such into my program. Therefore, I was wondering if it ... stackoverflow.com 구글링을 해보니 nodeJS에서 파이썬을 이용하는 방법은 두 가지가 있다고 한다. 하나는 아예 파이썬 스크립트를 같은 소스코드에 넣어놓고 Node에서 경로를 찾아 돌리는 것. 하지만 이는 임시처리만 될 것 같아 두번째 방법을 사용해보기로 했다. 두번째 방법은 파이썬으로 새로운 서버를 하나 더 띄우고, API 콜하는 방식으로 파이썬 서버와 NodeJS서버가 통신하는 것이다. 따라서 진행해야 할 일은 다음과 같다. 1) 파이썬으로 서버 구축 2) 파이썬 API에서 sklearn으로 result 만들기 3) Node서버와 연결 웹서버를 구축하는 프레임워크는 장고를 포함해 여러가지가 있으나, 나는 간단한 테스트용 API만 만들꺼라서 Flask를 사용하기로 했다. Flask를 이용한 서버구축은 간단하다. vscode의 python extension을 설치하고, pip install scikit-learn, pip install flask를 수행한다 app.py파일 하나만 존재하고, python cli를 이용해 스크립트를 실행해주면 된다. flask에서는 서버 객체인 app을 만들어줄 Flask와, response를 json형태로 보내줄 jsonify, 외부 API 콜의 request를 읽기 위한 request 객체를 이용한다. 또한 restAPI 구성은 flast_restx를 이용했다. 더불어 테스트를 위한 sklearn의 테스트 데이터들도 받았다. flask의 app.py를 실행하면, 내가 테스트해보려는 경로는 별도 지정해주지 않았을때 localhost와 5000포트가 된다. NodeJS를 활용해 개발한 API와 다를바 없이 서버화면이 랜더링된다. /test 경로는 API를 작성하기 위해 만들었는데 화면에 접속하면 get 메소드의 result가 랜더링된다. 세부 로직은 sklearn의 load_iris 데이터를 불러왔다 머신러닝의 많은 결과가 dataFrame이나 ndarray 객체이므로, response는 적절하게 파싱해주어야 한다. 이 API를 nodeJS 서버에서 콜해보았다. 리액트 뿐 아니라 nodeJS에서 하는 외부 콜도 axios를 사용한다. client를 통해서 call해보면 flask API에서 return한 결과가 서버에 잘 찍힌다. 상세한 학습 및 예측 로직을 구성하여 결과를 리턴해주면, NodeJS에서 잘 가공한 후 Client에게 제공할 수 있다. intermediate/flask_rest_api_tutorial Run in Google Colab Colab Download Notebook Notebook View on GitHub GitHub Note Click here to download the full example code Author: Avinash Sajjanshetty 번역: 박정환 이 튜토리얼에서는 Flask를 사용하여 PyTorch 모델을 배포하고 모델 추론(inference)을 할 수 있는 REST API를 만들어보겠습니다. 미리 훈련된 DenseNet 121 모델을 배포하여 이미지를 인식해보겠습니다. Tip 여기서 사용한 모든 코드는 MIT 라이선스로 배포되며, GitHub 에서 확인하실 수 있습니다. 이것은 PyTorch 모델을 상용(production)으로 배포하는 튜토리얼 시리즈의 첫번째 편입니다. Flask를 여기에 소개된 것처럼 사용하는 것이 PyTorch 모델을 제공하는 가장 쉬운 방법이지만, 고성능을 요구하는 때에는 적합하지 않습니다. 그에 대해서는:
API 정의¶먼저 API 엔드포인트(endpoint)의 요청(request)와 응답(response)을 정의하는 것부터 시작해보겠습니다. 새로 만들 API 엔드포인트는 이미지가 포함된 {"class_id": "n02124075", "class_name": "Egyptian_cat"} 의존성(Dependencies)¶아래 명령어를 실행하여 필요한 패키지들을 설치합니다: $ pip install Flask==2.0.1 torchvision==0.10.0 간단한 웹 서버¶Flask의 문서를 참고하여 아래와 같은 코드로 간단한 웹 서버를 구성합니다. from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return 'Hello World!' 위 코드를 $ FLASK_ENV=development FLASK_APP=app.py flask run 웹 브라우저로 API 정의에 맞게 위 코드를 조금 수정해보겠습니다. 먼저, 메소드의 이름을 @app.route('/predict', methods=['POST']) def predict(): return 'Hello World!' 또한, ImageNet 분류 ID와 이름을 포함하는 JSON을 회신하도록 응답 형식을 변경하겠습니다. 이제 from flask import Flask, jsonify app = Flask(__name__) @app.route('/predict', methods=['POST']) def predict(): return jsonify({'class_id': 'IMAGE_NET_XXX', 'class_name': 'Cat'}) 추론(Inference)¶다음 섹션에서는 추론 코드 작성에 집중하겠습니다. 먼저 이미지를 DenseNet에 공급(feed)할 수 있도록 준비하는 방법을 살펴본 뒤, 모델로부터 예측 결과를 얻는 방법을 살펴보겠습니다. 이미지 준비하기¶DenseNet 모델은 224 x 224의 3채널 RGB 이미지를 필요로 합니다. 또한 이미지 텐서를 평균 및 표준편차 값으로 정규화합니다. 자세한 내용은 여기 를 참고하세요.
import io import torchvision.transforms as transforms from PIL import Image def transform_image(image_bytes): my_transforms = transforms.Compose([transforms.Resize(255), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) image = Image.open(io.BytesIO(image_bytes)) return my_transforms(image).unsqueeze(0) 위 메소드는 이미지를 byte 단위로 읽은 후, 일련의 변환을 적용하고 Tensor를 반환합니다. 위 메소드를 테스트하기 위해서는 이미지를 byte 모드로 읽은 후 Tensor를 반환하는지 확인하면 됩니다. (먼저 ../_static/img/sample_file.jpeg 을 컴퓨터 상의 실제 경로로 바꿔야 합니다.) with open("../_static/img/sample_file.jpeg", 'rb') as f: image_bytes = f.read() tensor = transform_image(image_bytes=image_bytes) print(tensor) Out: tensor([[[[ 0.4508, 0.4166, 0.3994, ..., -1.3473, -1.3473, -1.3473], [ 0.5364, 0.4851, 0.4508, ..., -1.2959, -1.3130, -1.3302], [ 0.7248, 0.6392, 0.6049, ..., -1.2959, -1.3302, -1.3644], ..., [ 1.3584, 1.3755, 1.4098, ..., 1.1700, 1.3584, 1.6667], [ 1.8893, 1.7694, 1.4440, ..., 1.2899, 1.4783, 1.5468], [ 1.6324, 1.8379, 1.8379, ..., 1.4783, 1.7352, 1.4612]], [[ 0.5728, 0.5378, 0.5203, ..., -1.3529, -1.3529, -1.3529], [ 0.6604, 0.6078, 0.5728, ..., -1.3004, -1.3354, -1.3529], [ 0.8529, 0.7654, 0.7304, ..., -1.3004, -1.3354, -1.3704], ..., [ 1.4657, 1.4657, 1.4832, ..., 1.3256, 1.5357, 1.8508], [ 2.0084, 1.8683, 1.5182, ..., 1.4657, 1.6583, 1.7283], [ 1.7458, 1.9384, 1.9209, ..., 1.6583, 1.9209, 1.6408]], [[ 0.7228, 0.6879, 0.6531, ..., -1.6476, -1.6302, -1.6302], [ 0.8099, 0.7576, 0.7228, ..., -1.6302, -1.6302, -1.6476], [ 1.0017, 0.9145, 0.8797, ..., -1.6650, -1.6650, -1.6999], ..., [ 1.6291, 1.6291, 1.6465, ..., 1.6291, 1.8208, 2.1346], [ 2.1694, 2.0300, 1.6814, ..., 1.7685, 1.9428, 2.0125], [ 1.9254, 2.0997, 2.0823, ..., 1.9428, 2.2043, 1.9254]]]]) 예측(Prediction)¶미리 학습된 DenseNet 121 모델을 사용하여 이미지 분류(class)를 예측합니다. from torchvision import models # 이미 학습된 가중치를 사용하기 위해 `pretrained` 에 `True` 값을 전달합니다: model = models.densenet121(pretrained=True) # 모델을 추론에만 사용할 것이므로, `eval` 모드로 변경합니다: model.eval() def get_prediction(image_bytes): tensor = transform_image(image_bytes=image_bytes) outputs = model.forward(tensor) _, y_hat = outputs.max(1) return y_hat
import json imagenet_class_index = json.load(open('../_static/imagenet_class_index.json')) def get_prediction(image_bytes): tensor = transform_image(image_bytes=image_bytes) outputs = model.forward(tensor) _, y_hat = outputs.max(1) predicted_idx = str(y_hat.item()) return imagenet_class_index[predicted_idx]
with open("../_static/img/sample_file.jpeg", 'rb') as f: image_bytes = f.read() print(get_prediction(image_bytes=image_bytes)) Out: ['n02124075', 'Egyptian_cat'] 다음과 같은 응답을 받게 될 것입니다: ['n02124075', 'Egyptian_cat'] 배열의 첫번째 항목은 ImageNet 분류 ID이고, 두번째 항목은 사람이 읽을 수 있는 이름입니다. Note
모델을 API 서버에 통합하기¶마지막으로 위에서 만든 Flask API 서버에 모델을
추가하겠습니다. API 서버는 이미지 파일을 받는 것을 가정하고 있으므로, 요청으로부터 파일을 읽도록 from flask import request @app.route('/predict', methods=['POST']) def predict(): if request.method == 'POST': # we will get the file from the request file = request.files['file'] # convert that to bytes img_bytes = file.read() class_id, class_name = get_prediction(image_bytes=img_bytes) return jsonify({'class_id': class_id, 'class_name': class_name})
import io import json from torchvision import models import torchvision.transforms as transforms from PIL import Image from flask import Flask, jsonify, request app = Flask(__name__) imagenet_class_index = json.load(open('<PATH/TO/.json/FILE>/imagenet_class_index.json')) model = models.densenet121(pretrained=True) model.eval() def transform_image(image_bytes): my_transforms = transforms.Compose([transforms.Resize(255), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) image = Image.open(io.BytesIO(image_bytes)) return my_transforms(image).unsqueeze(0) def get_prediction(image_bytes): tensor = transform_image(image_bytes=image_bytes) outputs = model.forward(tensor) _, y_hat = outputs.max(1) predicted_idx = str(y_hat.item()) return imagenet_class_index[predicted_idx] @app.route('/predict', methods=['POST']) def predict(): if request.method == 'POST': file = request.files['file'] img_bytes = file.read() class_id, class_name = get_prediction(image_bytes=img_bytes) return jsonify({'class_id': class_id, 'class_name': class_name}) if __name__ == '__main__': app.run() 이제 웹 서버를 테스트해보겠습니다! 다음과 같이 실행해보세요: $ FLASK_ENV=development FLASK_APP=app.py flask run requests 라이브러리를 사용하여 POST 요청을 만들어보겠습니다: import requests resp = requests.post("http://localhost:5000/predict", files={"file": open('<PATH/TO/.jpg/FILE>/cat.jpg','rb')}) resp.json() 을 호출하면 다음과 같은 결과를 출력합니다: {"class_id": "n02124075", "class_name": "Egyptian_cat"} 다음 단계¶지금까지 만든 서버는 매우 간단하여 상용 프로그램(production application)으로써 갖춰야할 것들을 모두 갖추지 못했습니다. 따라서, 다음과 같이 개선해볼 수 있습니다:
Total running time of the script: ( 0 minutes 1.130 seconds) Gallery generated by Sphinx-Gallery |