↓폴밍끼 유튜브 채널 자세히보기

Python library & package/Tensorflow & keras

Tensorflow Model Server 정복 (REST API, WSL2 사용, LSTM 모델)

폴밍끼 2021. 7. 30. 21:03
728x90

저는 Tensorflow Model Server를 아무것도 모르는 상태에서 구현하기 위해 많은 애를 써야 했습니다. 그래서 저의 경험을 바탕으로 다른 분들은 조금이라도 쉽게 이에 접근하실 수 있도록 포스트를 작성합니다! 방법은 크게 5단계입니당!!

0. 일단은 학습한 모델을 다음과 같이 tensorflow의 saved_model 함수를 써서 저장해야 합니다. (여기엔 생략된 것이지 위쪽에 model=Sequential() 해서 LSTM 모델 학습을 시켜주었습니다)

import tensorflow as tf

data_path = 'C:\Users\007_0\serving\btc_lstm'
modelpath = data_path  + "/1" # 모델 버전 1을 의미합니다

tf.saved_model.save(model, modelpath)

saved_model 함수를 사용하면 다음과 같이 저장됩니다. 일반 모델 저장 함수와는 다르게 모델만 저장하는 것이 아닌, 모델의 한 버전을 나타내도록 저장됩니다.

btc_lstm
ㄴ1
   ㄴ assets
   ㄴ saved_model.pb
   ㄴ variables
            ㄴ variables.data-00000-of-00001
            ㄴ variables.index

saved_model.pb는 직렬화된 프로토콜 버퍼로 표현된 계산 그래프가 정의된 파일이고 variables는  변숫값을 담은 폴더입니다. assets는 어휘 사전 파일, 클래스 이름, 모델을 위한 샘플 데이터같은 부가적인 데이터가 들어있는 폴더입니다. 이 경우에는 assets 폴더는 비어 있을 것입니다.

 

1. TF 서빙을 설치하는 방법은 여러 가지입니다. 도커 Doker 이미지(사진이란 뜻의 이미지 아닙니다. '도커 이미지'가 무엇인지 궁금하신 분들은 클릭)를 사용하거나 시스템의 패키지 매니저를 사용하거나 소스에서 설치하는 등의 방법입니다. 설치가 간단하니 텐서플로 팀에서 추천하는 도커 옵션을 사용하겠습니다. 먼저 도커를 설치해야 합니다. 저는 윈도우 10 환경에 WSL2를 사용했기 때문에 도커 설치는 이 영상(WSL이 설치되어 있으시다면 8:15초부터 보세요.. / Docker Desktop을 실행하는 12:51초까지만 보셔도 됩니다:) )을 보고 따라했습니다. 그 다음 공식 TF 서빙 도커 이미지를 다운로드합니다.

$ docker pull tensorflow/serving

 

2. 이 이미지를 실행하기 위해 도커 컨테이너를 만듭니다.

$ docker run -d -p 8501:8501 --name tfserving_btc_lstm --mount type=bind,source=/mnt/c/Users/007_0/serving/btc_lstm,target=/models/btc_lstm -e MODEL_NAME=btc_lstm -t tensorflow/serving
☆위 도커 컨테이너 생성 코드 상세 설명☆
※ 굵게 표시된 부분을 제외한 부분(호스트 시스템의 디렉터리라든가 이름 등)만 각자에 맞게 바꿔 적어넣으시면 되겠습니다.
-it 인터렉티브 모드로 컨테이너를 만들기(따라서 Ctrl+C를 눌러 중지시킬 수 있음). 서버의 출력을 콜솔창에 나타냄.
-d 컨테이너를 백그라운드로 실행. 이 옵션 없이 실행한다면 해당 터미널에서 Ctrl + C를 눌러서 빠져나오는 순간 해당 컨테이너는 종료됨.
--rm 중지할 때 컨테이너를 삭제. (중지된 컨테이너 때문에 시스템이 지저분해지지 않음). 하지만 이미지를 삭제하지는 않음.
-p 8501:8501 호스트 시스템의 TCP 포트 8501번을 컨테이너의 TCP 포트 8501번으로 포워딩. 기본적으로 TF 서빙 시스템은 이 포트를 사용해 REST API를 제공. -> 예시처럼 그대로 포트 번호 8501:8501 사용하기.
--name tfserving_btc_lstm 이후에 이름으로 지칭할 수 있도록 우리가 만들고 있는 컨테이너의 이름을 'tfserving_coinlstm'으로 지정.
--mount type=bind,source=/mnt/c/Users/007_0/serving/btc_lstm,target=/models/btc_lstm 호스트 시스템의 [...]/btc_lstm 디렉터리를 컨테이너의 /models/btc_lstm 경로에 마운트(연결). Windows에서는 호스트 경로의 /를 \로 바꾸어야 함.(컨테이너 경로는 그대로 / 사용)

참고로 이 때 호스트 시스템의 경로는 saved_model로 모델을 저장해줬을 때 지정했던 경로 /mnt/c/Users/007_0/serving/btc_lstm/1에서 버전을 나타내는 1 직전인, /mnt/c/Users/007_0/serving/btc_lstm 까지만 입력하시면 됩니다.
-e MODEL_NAME=btc_lstm TensorFlow Serving에 'btc_lstm'으로 명명된 모델을 로드하도록 알림. 기본적으로 /models 디렉터리에서 모델을 찾고 자동으로 최신 버전을 서비스.
-t tensorflow/serving 제공 이미지 'tensorflow/serving'을 기준으로 Docker 컨테이너 실행
※ 주의 : -e MODEL_NAME의 이름을 마운트되는 도커의 models 의 하위 폴더 이름과 같게 설정해주세요. 주황색으로 표시해놓았습니다. 그렇지 않으면 컨테이너 생성 시 접근 에러가 떠서 생성되지 않습니다. 추가로 말씀드리자면 컨테이너 이름인 tfserving_btc_lstm 도 이와 같이 통일시킨 것입니다.(이 세 개를 통일하는 것이 좋은 것 같습니다)

위 코드를 실행하면 다음과 같이 도커 컨테이너가 실행됩니다. -d 옵션을 사용했기 때문에 실행 결과로 컨테이너 ID만을 콘솔창에 출력합니다.

docker container ls를 입력하여 컨테이너 생성을 체크할 수 있습니다

Docker Desktop으로 확인해봐도 컨테이너를 생성을 확인할 수 있습니다.

 

3. 도커로 TF 서버를 생성했으니 REST API로 TF서빙에 쿼리를 할 차례입니다. 다음 코드는 모두 VS Code에서 작성했습니다. colab에서 할 경우, 나중에 localhost 문제가 생기니 로컬 환경에서 VS Code 등에서 하실 것을 권장합니다. 먼저 다음과 같이 쿼리를 위한 JSON 데이터를 만듭니다. 여기에는 호출할 함수 시그니처의 이름 "serving_btc_lstm"(하고 싶은 이름 입력)과 입력 데이터 x_test.tolist()가 포함되어야 합니다.

import json

input_data_jason = json.dumps({
    "signiture_name": "serving_btc_lstm",
    "instances": x_test.tolist()
})

JSON 포맷은 100% 텍스트이므로 x_test 넘파이 배열을 파이썬 리스트로 변환한 후 JSON 문자열로 만들어야 합니다. input_data_json을 print해보면 다음과 같습니다. 여기서 x_test는 lstm모델에 사용할 것이므로 3차원 데이터입니다.

{"signiture_name": "serving_btc_lstm", "instances": [[[0.0, 0.0, 0.0, 0.0, 0.022956650367180968, 0.007650134769335071, 0.00031732599957690066], [0.0026272431758471715, 0.0026703499079189508, 0.002660539363889214, 0.0012671594889853421, 0.0, 0.0030523739006920736, 0.005595879532736631], [0.003314917127071826, 0.001602370298706024, 0.010418070899375936, 0.003414147727392975, 0.0019492882831152192, 0.004408424989980841, 0.0002762430939226568], [0.0027210061676139763, 0.025137596025019637, 0.008733864806299858, 0.0032337030433074454, 0.01237921360822905, 0.005263965623081651, 0.01100495827790543], [0.04426806205327818, 0.016321744072394735, 0.01121218531838375, 0.013863531786673794, 0.004527317372621242, 0.007195549643245869, 0.08096574448900892], [0.03030857240848601, 0.007585602466076341, 0.01122144542904216, 0.005570902394106819, 0.009417704680130617, 0.035083701546715536, 0.012905196906954786]], ...................................................................(중략)................................................................... [[0.3705007706488561, 0.3871398673019548, 0.35957642725598526, 0.38568750755835046, 0.2047998605896297, 0.21956891583284086, 0.38595908005681645], [0.39086550592985114, 0.34624002455494163, 0.3411537066150683, 0.2618377577268713, 0.2729566842291501, 0.3414125540209737, 0.3469742173932404], [0.3250767341927563, 0.3483643729592454, 0.235828091161836, 0.233397656093961, 0.34843905829731936, 0.3766754241439195, 0.3516421117249846], [0.3839793203531261, 0.21781189273453472, 0.22827531937510837, 0.3842213424400859, 0.38513603776105443, 0.38133824432166974, 0.3835862861289152], [0.22398243922451477, 0.24345355852542905, 0.3840097917737012, 0.4090335604340146, 0.3908839779005525, 0.40730438988995044, 0.23367892455699607], [0.26022312741050874, 0.4075372480280456, 0.4038829763548114, 0.3939533456108042, 0.39439472729471514, 0.15168175873758388, 0.17580566948395102]]]}

 

4. 이 입력 데이터를 HTTP POST 메서드로 TF서빙에 전송할 것입니다. requests 라이브러리를 사용해 쉽게 처리할 수 있습니다. (표준 파이썬 라이브러리가 아니므로 설치를 안 하셨다면 pip 명령으로 먼저 설치해주세요.) 여기선 도커 TF서버 url은 'http://localhost:8501/v1/models/{btc_lstm}:predict' 에서 굵게 표시된 부분만 아까 컨테이너 생성 시 model/{하위 폴더}로 지정했던 이름으로 바꿔주시면 됩니다.

import requests

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

response = requests.post(
    SERVER_URL, data=input_data_jason)
response.raise_for_status() # 에러 생기면 예외 발생
response = response.json()

response객체는 "predictions" 키 하나를 가진 딕셔너리입니다. 이 키에 해당하는 값은 서버에 존재하는 모델에서 JSON x_test값을 읽어 들여 반환한 예측 결괏값 리스트입니다. 이 리스트는 파이썬 리스트이므로 넘파이 배열로 변환합니다.

preds = np.array(response["predictions"])

preds를 print 해보면 다음과 같이 결과가 잘 나오는 것을 볼 수 있습니다. (사실 아래 결과는 preds를 바로 출력한 것이 아니라 역 스케일링을 해주고 첫 번째 열의 데이터들만 출력한 것입니다)

[31801231.61133 32519847.52778 31427238.22548 29831188.32514 28198921.91869 28257152.86739 30345947.74358 30671549.79049 31540416.23814 32384378.97790 30376233.70821 29207232.95664 25872081.40142 24152682.59159 27619602.68470 31212749.23910 31879733.41084 32101310.65662 .................................................................(중략)................................................................. 45414338.78426 45952055.21855 46682847.82083 46603872.71626 45429477.79590 43846109.58570 43026587.04312 40958660.60509 38194517.25355 37698618.56511 39476212.78604 40132836.72570 39272330.61097 39876222.26578 40864919.20338 41868356.76104 42601624.55287]

 

이상입니다. 참고가 되실까 하여 저의 lstm 모델을 활용한 예측에 쓰일 x_test데이터를 만들고 REST API로 TF서빙에 쿼리를 하여 예측값까지 프린트해보는 전체 코드(VS Code로 작성한 test.py 파일)를 보여드리겠습니다.

import numpy as np
import pandas as pd
import pyupbit
from sklearn.preprocessing import MinMaxScaler
import json
import requests

# 결과확인을 위해 넘파이 소수 출력 옵션 변경
np.set_printoptions(formatter={'float_kind': lambda x: "{0:0.5f}".format(x)})


def create_dataset(dataset, look_back=7, foresight=(30-1)):
    X, Y = [], []

    for i in range(dataset.shape[0]-look_back - foresight):
        obs = dataset[i:(i+look_back), :]
        X.append(obs)
        Y.append(dataset[i+(look_back+foresight), 0])

    return np.array(X), np.array(Y)


print('데이터 불러오는 중')
df = pyupbit.get_ohlcv("KRW-BTC", count=1500)
df.drop_duplicates(inplace=True)
array = df.to_numpy()

print('테스트 데이터 생성')
fourfifth = array.shape[0]-int(array.shape[0]/5)
test_data = array[fourfifth:]
scaler = MinMaxScaler()
test_data = scaler.fit_transform(test_data)

print('x_test 생성')
x_test, _ = create_dataset(test_data)

x_test = np.reshape(
    x_test, (x_test.shape[0], x_test.shape[2], x_test.shape[1]))

print('json데이터 생성')
input_data_jason = json.dumps({
    "signiture_name": "serving_btc_lstm",
    "instances": x_test.tolist()
})

print('도커 서버에 요청')
SERVER_URL = 'http://localhost:8501/v1/models/btc_lstm:predict'

response = requests.post(
    SERVER_URL, data=input_data_jason)
response.raise_for_status()
response = response.json()

print('예측')
preds = np.array(response["predictions"])
# print(preds)
# 결과 왜 소수로 나옴? 그야 역스케일링을 안해줬기 때문이지!
preds = np.concatenate(
    [preds, preds, preds, preds, preds, preds], axis=1)  # (n, 6)
preds_inversed = scaler.inverse_transform(preds)
preds_inversed = preds_inversed[:, 0]

print(preds_inversed)

덧) REST API는 간편하고 좋습니다. 입력과 출력 데이터가 너무 크지 않으면 잘 작동합니다. 하지만 JSON 기반이므로 텍스트를 사용하고 매우 장황합니다. 이는 큰 넘파이 배열을 전송할 때 응답 속도를 느리게 하고 네트워크 대역폭을 많이 사용합니다.(사실 REST 요청을 하기 전에 먼저 데이터를 직렬화하고 Base64로 인코딩하여 이를 완화할 수 있습니다. 또한 gzip을 사용해 REST요청을 압축하면 페이로드 크기를 크게 줄일 수 있습니다.) 그래서 대량의 데이터를 전송할 땐 클라이언트가 지원한다면 gRPC API를 사용하는 것이 훨씬 좋습니다. 컴팩트한 이진 포맷과 HTTP/2 프레임에 기반한 효율적인 통신 프로토콜을 사용하기 때문입니다.

gRPC API로 TF 서빙에 쿼리하는 방법도 포스팅하고 싶으나 문제도 해결했고 저의 의욕이 다한 관계로ㅜㅜ 여기서 마무리하겠습니다~! 다음에 기회가 된다면 이것도 올려 볼게요ㅎㅎ

 

도움을 주신 분들♡ : 내가 며칠 동안 끙끙 앓은 시간들, 다수의 구글 문서 작성자분들, 다수의 유튜버분들, 도서 '핸즈온 머신러닝 :사이킨럿, 케라스, 텐서플로 2를 활용한 머신러닝, 딥러닝 완벽 실무(한빛미디어 , 2020)'의 저자 오렐리앙 제롱님