텐서플로우 서빙(TensorFlow Serving)
텐서플로우 서빙(TensorFlow Serving)[2]은 구글에서 만든 프로덕션(production) 환경을 위한 유연하고(flexible), 고성능의(high-performance) serving 시스템이다.
보통 모델 설계 및 트레이닝이 끝나면 이를 실제 프로덕션 환경에 응용하기 위해서 추론(Inference)을 수행할 수 있는 시스템을 구축해야하는데 TensorFlow Serving은 이 과정을 최적화된 형태로 지원한다.
Fashion MNIST 분류 CNN 학습 및 SavedModel 포맷으로 모델 저장
Serving 사용법을 익히기 위해서 Fashion MNIST 데이터셋을 분류하는 간단한 CNN 모델을 학습시키고 이를 TensorFlow Serving에서 로드할 수 있는 SavedModel 포맷[3]으로 저장해보자.
Fashion MNIST 데이터셋은 70,000장의 28×28 크기의 그레이스케일(grayscale) 패션 아이템들로 구성된 데이터셋으로 총 10개의 레이블을 가지고 있다.
그림 1 – Fashion MNIST 데이터셋
Fashion MNIST 데이터셋에 대한 간단한 CNN 분류기를 학습시키고, 이를 Serving에서 로드할 수 있는 SavedModel 포맷[3]으로 저장하는 예제 코드는 아래와 같다.
# TensorFlow Serving Example (Simple REST API using Fashion MNIST)
# Reference : https://www.tensorflow.org/tfx/tutorials/serving/rest_simple
import tensorflow as tf
from tensorflow import keras
import os
def make_directory(target_path):
if not os.path.exists(target_path):
os.mkdir(target_path)
print('Directory ', target_path, ' Created ')
else:
print('Directory ', target_path, ' already exists')
print('TensorFlow version: {}'.format(tf.__version__))
fashion_mnist = keras.datasets.fashion_mnist
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
# scale the values to 0.0 to 1.0
train_images = train_images / 255.0
test_images = test_images / 255.0
# reshape for feeding into the model
train_images = train_images.reshape(train_images.shape[0], 28, 28, 1)
test_images = test_images.reshape(test_images.shape[0], 28, 28, 1)
class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
print('\ntrain_images.shape: {}, of {}'.format(train_images.shape, train_images.dtype))
print('test_images.shape: {}, of {}'.format(test_images.shape, test_images.dtype))
model = keras.Sequential([
keras.layers.Conv2D(input_shape=(28,28,1), filters=8, kernel_size=3,
strides=2, activation='relu', name='Conv1'),
keras.layers.Flatten(),
keras.layers.Dense(10, activation=tf.nn.softmax, name='Softmax')
])
model.summary()
epochs = 5
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=epochs)
test_loss, test_acc = model.evaluate(test_images, test_labels)
print('\nTest accuracy: {}'.format(test_acc))
# Fetch the Keras session and save the model
# The signature definition is defined by the input and output tensors,
# and stored with the default serving key
SAVED_MODEL_PATH = './saved_model'
make_directory(SAVED_MODEL_PATH)
MODEL_DIR = SAVED_MODEL_PATH
version = 1
export_path = os.path.join(MODEL_DIR, str(version))
print('export_path = {}\n'.format(export_path))
tf.keras.models.save_model(
model,
export_path,
overwrite=True,
include_optimizer=True,
save_format=None,
signatures=None,
options=None
)
print('\nSaved model:')
위 코드를 실행하면 Fashion MNIST 데이터 분류에 대한 CNN이 학습되고, 학습된 CNN 모델이 스크립트가 실행된 경로 아래에 ./saved_model/1 경로에 SavedModel 포맷으로 저장된다.
Docker를 이용한 TensorFlow Serving 실행
모델을 SavedModel 포맷으로 저장한 이후에 이를 TensorFlow Serving에서 로드해서 API 서버로 만들 수 있다.
보통 TensorFlow Serving은 Docker를 이용해서 실행한다. Docker를 이용해서 TensorFlow Serving을 실행하는 방법은 아래와 같다.
먼저 docker pull 명령어로 TensorFlow Serving 이미지를 다운받는다.
> docker pull tensorflow/serving |
다음으로 아래와 같은 명령어로 TensorFlow Serving을 이용해서 저장된 모델에 대한 REST API 추론 서버를 실행할 수 있다.
> docker run -t --rm -p 8501:8501 \ -v "/home/solaris/Desktop/tf_serving/saved_model:/models/fashion_model" \ -e MODEL_NAME=fashion_model \ tensorflow/serving & |
각각의 명령어 인자값에 대한 설명은 아래와 같다.
- -p : 서버에서 데이터를 주고받는데 사용할 포트(Port) 번호를 지정한다. (위 예시의 경우, 8501 포트)
- -v : 불러올 모델이 SavedModel 포맷으로 저장된 전체 경로(full path)를 의미한다. (위 예시의 경우, home/solaris/Desktop/tf_serving/saved_model 경로에서 모델 파일을 불러온다.), 뒤에는 모델을 실행할 REST API URL을 의미한다. (위 예시의 경우, models/fashion_model)
TensorFlow Serving 서버가 잘 실행되었다면 아래와 같이 8501 포트로 REST API 서버가 구성되었다는 로그를 볼 수 있다.
2020-05-30 10:38:09.443951: I tensorflow_serving/model_servers/server.cc:86] Building single TensorFlow model file config: model_name: fashion_model model_base_path: /models/fashion_model 2020-05-30 10:38:09.444251: I tensorflow_serving/model_servers/server_core.cc:462] Adding/updating models. 2020-05-30 10:38:09.444283: I tensorflow_serving/model_servers/server_core.cc:573] (Re-)adding model: fashion_model 2020-05-30 10:38:09.544965: I tensorflow_serving/core/basic_manager.cc:739] Successfully reserved resources to load servable {name: fashion_model version: 1} 2020-05-30 10:38:09.545018: I tensorflow_serving/core/loader_harness.cc:66] Approving load for servable version {name: fashion_model version: 1} 2020-05-30 10:38:09.545046: I tensorflow_serving/core/loader_harness.cc:74] Loading servable version {name: fashion_model version: 1} 2020-05-30 10:38:09.545085: I external/org_tensorflow/tensorflow/cc/saved_model/reader.cc:31] Reading SavedModel from: /models/fashion_model/1 2020-05-30 10:38:09.549158: I external/org_tensorflow/tensorflow/cc/saved_model/reader.cc:54] Reading meta graph with tags { serve } 2020-05-30 10:38:09.549198: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:264] Reading SavedModel debug info (if present) from: /models/fashion_model/1 2020-05-30 10:38:09.549395: I external/org_tensorflow/tensorflow/core/platform/cpu_feature_guard.cc:142] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 AVX512F FMA 2020-05-30 10:38:09.585414: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:203] Restoring SavedModel bundle. 2020-05-30 10:38:09.607762: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:152] Running initialization op on SavedModel bundle at path: /models/fashion_model/1 2020-05-30 10:38:09.613230: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:333] SavedModel load for tags { serve }; Status: success: OK. Took 68142 microseconds. 2020-05-30 10:38:09.613782: I tensorflow_serving/servables/tensorflow/saved_model_warmup.cc:105] No warmup data file found at /models/fashion_model/1/assets.extra/tf_serving_warmup_requests 2020-05-30 10:38:09.614102: I tensorflow_serving/core/loader_harness.cc:87] Successfully loaded servable version {name: fashion_model version: 1} 2020-05-30 10:38:09.619644: I tensorflow_serving/model_servers/server.cc:358] Running gRPC ModelServer at 0.0.0.0:8500 ... [warn] getaddrinfo: address family for nodename not supported 2020-05-30 10:38:09.624274: I tensorflow_serving/model_servers/server.cc:378] Exporting HTTP/REST API at:localhost:8501 ... [evhttp_server.cc : 238] NET_LOG: Entering the event loop ... |
POST Request를 통한 이미지 데이터 전송 및 예측 결과 시각화
이제 TensorFlow Serving을 실행할때 지정한 아래 URL(models/fashion_model)로 인풋 데이터를 전송한 후, 해당 인풋 데이터에 대한 예측 결과값을 반환받을 수 있다.
http://localhost:8501/v1/models/fashion_model:predict |
만약 버전에 대한 정보까지 지정해서 요청하고자 할 경우, 요청 URL은 아래와 같다.
http://localhost:8501/v1/models/fashion_model/versions/1:predict |
실행된 서버에 POST Request로 Fashion MNIST 테스트 이미지를 3개 전송하고, API 서버로부터 반환받은 예측결과를 시각화해보는 예제 코드는 아래와 같다.
from tensorflow import keras
import matplotlib.pyplot as plt
import numpy as np
import random
import json
import requests
def show(idx, title):
plt.figure(figsize=(12, 3))
plt.imshow(test_images[idx].reshape(28,28))
plt.axis('off')
plt.title('\n\n{}'.format(title), fontdict={'size': 16})
plt.show()
fashion_mnist = keras.datasets.fashion_mnist
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
# scale the values to 0.0 to 1.0
test_images = test_images / 255.0
# reshape for feeding into the model
test_images = test_images.reshape(test_images.shape[0], 28, 28, 1)
class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
print('test_images.shape: {}, of {}'.format(test_images.shape, test_images.dtype))
rando = random.randint(0,len(test_images)-1)
show(rando, 'An Example Image: {}'.format(class_names[test_labels[rando]]))
data = json.dumps({"signature_name": "serving_default", "instances": test_images[0:3].tolist()})
print('Data: {} ... {}'.format(data[:50], data[len(data)-52:]))
# send data using POST request and receive prediction result
headers = {"content-type": "application/json"}
json_response = requests.post('http://localhost:8501/v1/models/fashion_model:predict', data=data, headers=headers)
predictions = json.loads(json_response.text)['predictions']
# show first prediction result
show(0, 'The model thought this was a {} (class {}), and it was actually a {} (class {})'.format(
class_names[np.argmax(predictions[0])], np.argmax(predictions[0]), class_names[test_labels[0]], test_labels[0]))
# set model version and send data using POST request and receive prediction result
json_response = requests.post('http://localhost:8501/v1/models/fashion_model/versions/1:predict', data=data, headers=headers)
predictions = json.loads(json_response.text)['predictions']
# show all prediction result
for i in range(0,3):
show(i, 'The model thought this was a {} (class {}), and it was actually a {} (class {})'.format(
class_names[np.argmax(predictions[i])], np.argmax(predictions[i]), class_names[test_labels[i]], test_labels[i]))
TensorFlow Serving을 이용할 경우 위와 같이 간단하게 모델에 대한 REST API 형태의 추론 서버를 만들 수 있다.