Project/[약올림] Final Project

[Milestone 그 이후] Tensorflow Model Serving

HJChung 2021. 1. 22. 11:05

이미치 출처: https://neptune.ai/blog/how-to-serve-machine-learning-models-with-tensorflow-serving-and-docker

1. SavedModel로 내보내기

학습시킨 모델을 tf.saved_model.save()함수로 SavedModel 포맷으로 내보낼 수 있다. 

모델과 이름, 버전을 포함한 경로를 전달하면 이 함수는 이 경로에 모델의 계산 그래프와 학습결과 가중치를 저장한다. 

model_version = "0001"
model_name = "medisharp_pill_image_model"
model_path = os.path.join(model_name, model_version)
tf.saved_model.save(model, model_path)

그러면

- saved_model.pb: 계산 그래프를 정의

- variables: 변수값을 담고있는 폴더로, 많은 개수의 가중치를 담은 모델의 경우 변수값이 여러개의 파일로 나뉘어 저장 될 수 있다. 

- assets: 부가적인 데이터가 들어있는 폴더를 포함하지만 내 경우 사용하지 않았다.

 

이런 형식으로 저장된다. 

 

2. 저장된 모델 보기

텐서플로는 SavedModel을 검사할 수 있는 saved_model_cli 명령줄 도구를 제공한다. 한 번 보자. 

$ export ML_PATH="/Users/jeonghyeonjeong/Desktop/medisharp/medisharp-server/cnn/model"
$ cd ML_PATH
$ saved_model_cli show --dir medisharp_pill_image_model/0001 --all

3. Tensorflow Serving 설치하기

도커를 켜고 로그인을 한 후 Docker Desktop is running상태가 되면

공식 tf서빙 도커 이미지를 다운로드한다.

$ docker pull tensorflow/serving

그리고 이 이미지를 실행하기위해 도커 컨테이너를 만든다.

$ docker run -it --rm -p 8500:8500 -p 8501:8501 \
    -v "/Users/jeonghyeonjeong/medisharp_pill_image_model:/models/medisharp_pill_image_model" \
    -e MODEL_NAME=medisharp_pill_image_model \
    tensorflow/serving

※ [Docker] 각 명령 옵션이 의미하는 바 

-it: 인터랙티브 모드로 컨테이너를 만들고, 서버의 출력을 화면에 나타낸다. Ctrl+C로 중지할 수 있다. 

--rm: 중지할 때 컨테이너를 삭제함으로써 시스템이 지저분해지지 않게 한다. (이미지는 삭제되지 않는다.)

-p 8500:8500: 도커 엔진이 호스트 시스템의 TCP포트 8500번을 컨테이너의 TCP 8500번으로 포워딩한다. 기본적으로 TF서빙은 이 포트를 사용해 gRPC API를 제공한다. 

-p 8501:8501:  호스트 시스템의 TCP포트 8501번을 컨테이너의 TCP 8501번으로 포워딩한다. 기본적으로 TF서빙은 이 포트를 사용해 REST API를 제공한다. 

-v "/Users/jeonghyeonjeong/medisharp_pill_image_model:/models/medisharp_pill_image_model" : 호스트 시스템의 /Users/jeonghyeonjeong/medisharp_pill_image_model 디렉터리를 컨테이너의 /models/medisharp_pill_image_model경로에 연결한다.

-e MODEL_NAME=medisharp_pill_image_model: TF서빙이 어떤 모델을 서빙할지 알 수 있도록 컨테이너의 MODEL_NAME 환경 변수를 설정한다. 기본적으로 /models 디렉터리에서 모델을 찾고, 자동으로 최신 버전을 서빙한다. 

tensorflow/serving: 실행할 이미지명

 

※이 과정에서 아래와 같은 "docker: Error response from daemon: Mounts denied:" 에러가 발생한다면?

맥에서 docker container로 복사할 때는, 정해진 디렉토리인 /Users, /Volumes, /private, /tmp 가 기본으로 filesharing에 지정되어 있다. 기본 디렉토리 외에 다른 디렉토리를 docker container로 복사하려고 할 때 이런 에러가 발생할 수 있다. 

해결 방법은 공식문서에 잘 나와있다 :)) docs.docker.com/docker-for-mac/

 

여기까지 정상적으로 잘 되었다면 이제 python에서 이 서버를 호출하여서 REST API로 TF서빙에 쿼리를 날려볼 수 있다. 

 

4. REST API로 TF 서빙에 쿼리하기

1) client로 부터 파일전송된 이미지로 먼저 쿼리를 위한 JSON 데이터를 만든다. 

import json
import requests

def prepare_image(image, target):
	if image.mode != "RGB":
		image = image.convert("RGB")
	image = image.resize(target)

	image = img_to_array(image)
	image = np.expand_dims(image, axis=0)

	return image

image = request.files["image"].read()
image = Image.open(io.BytesIO(image))
image = prepare_image(image, target=(224, 224))

input_data_json = json.dumps({
	"signature_name": "serving_default", #호출할 함수 시그니처의 이름
	"instances": image.tolist() #입력 데이터 (JSON 포멧이므로 numpy 배열을 리스트형식으로 변환해야 한다. tolist()를 해주지 않으면 Object of type 'ndarray' is not JSON serializable 라는 에러가 발생)
})

2) 이제 이 input_data_json을 HTTP POST 메서드로 TF서빙에 전송한다.

SERVER_URL = 'http://localhost:8501/v1/models/medisharp_pill_image_model:predict'

response = requests.post(SERVER_URL, data=input_data_json)
response.raise_for_status() #에러 발생시 예외 발생시킴
response = response.json() 
print("response is: ", response)

"""
출력결과는 이런 식으로, 응답(response)은 "predictions" 키 하나를 가진 딕셔너리로, 이 키에 해당하는 값은 예측의 리스트이다.
response is:  {'predictions': [[0.00994166359, 0.00910509098, 
...
]}
"""

응답은 "predictions" 키 하나를 가진 딕셔너리로, 이 키에 해당하는 값은 예측의 리스트이다. 

 

3) 그리고 이 중 예측확률이 가장 큰 것으로 client로 결과를 응답해준다. 

 output_data = response["predictions"]

pred_class = np.argmax(output_data, axis=-1)
prediction_result = class_list[int(pred_class)]
print("prediction: ", class_list[int(pred_class)])
          
response_object = {
            'status': 'OK',
            'message': 'Successfully predict image class.',
            'prediction': prediction_result
}
return response_object, 200

4) postman으로 테스트해 보면~!!

요청에 따른 결과를 잘 응답해준다! tf서빙 성공!

 

전체 코드는 <더보기> 클릭!

더보기
@api.route('/image')
class PredictMedicineName(Resource):
  def post(self):
    """카메라로 촬영한 이미지를 서버로 보내오고, 학습된 모델에서 예측결과를 client에게 전달해주는 API"""
    try: 
      class_list =  get_class_list()
      try:
        token = request.headers.get('Authorization')
        decoded_token = jwt.decode(token, jwt_key, jwt_alg)
        user_id = decoded_token['id']
        if decoded_token: 
          file = request.files['image']
          if file.filename == '':
            response_object = {
              'status': 'Bad Request',
              'message': 'No Selected File.',
              }
            return response_object, 400
          elif file and file.filename:
          	#Ready for the Data
            image = request.files["image"].read()
            image = Image.open(io.BytesIO(image))
            image = prepare_image(image, target=(224, 224))

            # input
            input_data_json = json.dumps({
              "signature_name": "serving_default",
              "instances": image.tolist()
            })

            SERVER_URL = 'http://localhost:8501/v1/models/medisharp_pill_image_model:predict'

            response = requests.post(SERVER_URL, data=input_data_json)
            response.raise_for_status() #에러 발생시 예외 발생시킴
            response = response.json() #응답은 "predictions" 키 하나를 가진 딕셔너리로, 이 키에 해당하는 값은 예측의 리스트

            output_data = response["predictions"]

            pred_class = np.argmax(output_data, axis=-1)
            prediction_result = class_list[int(pred_class)]
            print("prediction: ", class_list[int(pred_class)])
          
            response_object = {
              'status': 'OK',
              'message': 'Successfully predict image class.',
              'prediction': prediction_result
            }
            return response_object, 200
      except Exception as e: 
        print("predict pill name 401 error: ", e) 
        response_object = {
          'status': 'fail',
          'message': 'Provide a valid auth token.',
        }
        return response_object, 401
    except Exception as e:
        response_object = {
          'status': 'Internal Server Error',
          'message': 'Some Internal Server Error occurred.',
        }
        return response_object, 500

 

이제 기존에 서버 코드에 직접 tflite모델을 저장하고, 그걸 서버 시작때마다 load()해오는 코드(아래 코드)가 없어도 된다 ㅎㅎ.

#Load TFLite model and allocate tensors.
# def load_model():
#   global interpreter
#   currdir = os.getcwd()
#   print("currdir: ", currdir)
#   modeldir = os.path.join(currdir+"/cnn/model/medisharp_tflite_model.tflite")
#   interpreter = tf.lite.Interpreter(model_path=modeldir)

# print(("* Loading Keras model and Flask starting server..."
# 		"please wait until server has fully started"))
# load_model()

물론 배포환경에서 확인을 해봐야겠지만 말이다. :))

또 대량의 데이터를 전송할 때는 gRPC API를 사용하는게 훨씬 좋다고 한다.

Image Classification on Tensorflow Serving with gRPC or REST Call for Inference

을 참고해서 이 역시 시도해 보면 좋겠다.

reference

핸즈온 머신러닝 개정2판 19장 대규모 텐서플로 모델 훈련과 배포

How to Serve Machine Learning Models with TensorFlow Serving and Docker

Tensorflow 2.0을 활용한 딥러닝 모델 서빙하기

Postman으로 파일전송 테스트 하기