Skip to main content
Version: 1.3

모델 서빙하기

시작하면서#

이 장에서는 앞서 만들었던 모델을 서비스로 제공할 수 있는 서버를 만들어 보겠습니다.
CAP은 생성된 모델을 간단하게 서빙 서버로 만들어주는 bentoML이라는 라이브러리를 제공합니다.
모델을 서빙하는 서버를 만드는 것은 서버 엔지니어링 지식이 필요한 작업이지만,
bentoML을 이용하면 모델 API 서버를 만들 수 있습니다.

info

bentoML 은 학습시킨 모델을 간단하게 API 서버로 만들 수 있는 컨테이너 이미지로 빌드할 수 있게 해줍니다.
사용되는 코드는 아래 링크에서 보실 수 있습니다.
07.fashion_mnist_serving.ipynb
08.building_serving_server.ipynb
09.deploy_serving_server.ipynb

:::

Goal#

  • 모델 API 서버 만들기
  • 모델 API 서버 배포하기
  • 모델 API 서버 테스트해보기

모델 API 서버 만들기#

앞서 만든 fashion mnist 모델을 가지고 모델 API 서버를 만들어 보겠습니다.
여기서는 bentoML과 fairing의 builder를 사용하여 모델 API 서버 컨테이너 이미지를 만들어 봅니다.


모델 준비하기#

먼저 fashion_mnist_serving.ipynb 라는 빈 노트북을 만들고 사용했던 MyModel 코드를 가져오겠습니다.
우리는 Tuner로 모델 튜닝하기에서 조금 더 나은 Validation accuracy를 가지는 하이퍼파라미터를 확보를 했습니다.

best_trial

이 값을 MyModel의 parser.add_argument의 default 값으로 변경하겠습니다.

argparse는 노트북에서는 정상적으로 실행되지 않습니다.
easydict나 namespace 등의 방법들이 있지만 튜토리얼에서는 간단하게 우회하도록 하겠습니다.
::: args = parser.parse_args() 코드를 fairing의 적용여부로 분기하도록 적용했습니다.

fashion_mnist_serving.ipynb
# fashion_mnist_serving.ipynbimport osimport datetimeimport tensorflow as tfimport argparsefrom tensorflow.python.keras.callbacks import Callback

class MyModel(object):    def train(self):        parser = argparse.ArgumentParser()        parser.add_argument('--node_amount', required=False, type=int, default=256)        parser.add_argument('--epoch', required=False, type=int, default=27)        parser.add_argument('--dropout_rate', required=False, type=float, default=0.283)        parser.add_argument('--optimizer', required=False, type=str, default="adam")        # argparse fairing 적용여부 분기        if os.getenv('FAIRING_RUNTIME', None) is None:            args = parser.parse_args(args=[])        else:            args = parser.parse_args()
        mnist = tf.keras.datasets.fashion_mnist        (x_train, y_train), (x_test, y_test) = mnist.load_data()
        print("x_train shape:", x_train.shape, "y_train shape:", y_train.shape)        print("x_test shape:", x_test.shape, "y_test shape:", y_test.shape)
        x_train, x_test = x_train / 255.0, x_test / 255.0
        model = tf.keras.models.Sequential([            tf.keras.layers.Flatten(input_shape=(28, 28)),            tf.keras.layers.Dense(args.node_amount, activation='relu'),            tf.keras.layers.Dropout(args.dropout_rate),            tf.keras.layers.Dense(10, activation='softmax')        ])
        model.compile(optimizer=args.optimizer,                        loss='sparse_categorical_crossentropy',                        metrics=['acc'])

        date_folder = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")        if os.getenv('FAIRING_RUNTIME', None) is None:            log_dir = "log/fit/" + date_folder            args = parser.parse_args(args=[])        else:            args = parser.parse_args()            log_dir = "/notebook/log/fit/" + date_folder
        print(f"tensorboard log dir : {log_dir}")
        tensorboard_cb = tf.keras.callbacks.TensorBoard(log_dir=log_dir,                                                        histogram_freq=1)        model.fit(x_train, y_train,                    verbose=0,                    validation_data=(x_test, y_test),                    epochs=args.epoch,                    callbacks=[LoggingTrain(),                              tensorboard_cb])        return model
def p(msg):    dt_now = datetime.datetime.now()    strftime = dt_now.strftime('%Y-%m-%dT%H:%M:%SZ')    print(f"{strftime} {msg}", flush=True)
class LoggingTrain(Callback):    """logging for train    """    def on_batch_end(self, batch, logs={}):        if batch % 100 == 0:            p(f"batch: {batch}")            p(f"accuracy={logs.get('acc')} loss={logs.get('loss')}")
    def on_epoch_begin(self, epoch, logs={}):        p(f"epoch: {epoch}")
    def on_epoch_end(self, epoch, logs={}):        p(f"Validation-accuracy={logs.get('val_acc')}")        p(f"Validation-loss={logs.get('val_loss')}")        return

이전 MyModel 코드와 달라진 점이 2가지가 있습니다.

1. parser argument들의 default 값#

  parser = argparse.ArgumentParser()  parser.add_argument('--node_amount', required=False, type=int, default=256)  parser.add_argument('--epoch', required=False, type=int, default=27)  parser.add_argument('--dropout_rate', required=False, type=float, default=0.283)  parser.add_argument('--optimizer', required=False, type=str, default="adam")
important

Tuner를 이용해 얻은 최적의 하이퍼 파라미터 값을 기본값으로 변경했습니다
Tuner 에서 Best trial's params로 알려준 값을 넣어주세요

2. argparse 분기#

   # argparse fairing 적용여부로 분기  if os.getenv('FAIRING_RUNTIME', None) is None:      args = parser.parse_args(args=[])  else:      args = parser.parse_args()

수정이 완료 되면, 셀을 하나 더 추가하여 변경된 값으로 학습을 진행해 보곘습니다.

my model 학습 진행#

my_model = MyModel()model = my_model.train()

train_model

이제 모델이 준비되었으니 모델 API 서버를 만들어 봅시다.


모델 API 서버 코드 만들기#

먼저 bentoML 패키지가 설치되어 있는지 확인합니다. 만약 설치가 안되어 있다면 설치합니다.

check_bentoml
!pip freeze | grep BentoML>BentoML==0.13.1  # 출력되지 않는다면!pip install bentoml # <- 실행하여 설치

아래의 코드는 bentoml의 keras 모델 API 서비스 클래스입니다.
KerasFashionMnistService란 클래스로 정의하였습니다.
아래 코드를 셀을 하나 추가하여 붙여넣습니다.

fashion_mnist_serving.ipynb
%%writefile keras_fashion_mnist_server.py# fashion_mnist_serving.ipynbfrom typing import Listimport numpy as npfrom PIL import Imagefrom bentoml import api, artifacts, env, BentoServicefrom bentoml.frameworks.keras import KerasModelArtifactfrom bentoml.adapters import ImageInput
class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',                'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']@env(docker_base_image="dudaji/cap-jupyterlab:tf2.0-cpu")@artifacts([KerasModelArtifact('classifier')])class KerasFashionMnistService(BentoService):    @api(input=ImageInput(pilmode='L'), batch=True)    def predict(self, imgs: List[np.ndarray]) -> List[str]:        inputs = []        for img in imgs:            img = Image.fromarray(img).resize((28,28))            img = np.array(img.getdata()).reshape((28,28))            inputs.append(img)        inputs = np.stack(inputs)        class_idxs = self.artifacts.classifier.predict_classes(inputs)        return [class_names[class_idx] for class_idx in class_idxs]

코드가 복잡해 보이긴 하지만 실제로 모델 API 서버에 대한 코드는 몇개 없습니다.

  • 환경 설정
  @env(docker_base_image="dudaji/cap-jupyterlab:tf2.0-cpu")

@env 데코레이터를 이용하여 설정이 가능합니다. 해당 코드에서는 API 서버 컨테이너의 베이스 이미지를 정의합니다.

  • Artifact 설정
@artifacts([KerasModelArtifact('classifier')])

@artifacts 데코레이터를 이용하여 설정이 가능합니다. 서비스를 컨테이너화 및 패키징하기 위해 사용되는 모델의 이름입니다.

artifact주의사항

두가지 주의할 사항이 있습니다.

  1. 꼭 이름을 classifier로 하실 필요는 없습니다. 단 저 모델의 이름은
    classidxs = self.artifacts.**_classifier.predict_classes(inputs) 부분과
    추후 있을 pack 작업에서 모두 같게 통일해주어야 합니다.
    fashion_mnist_svc.
    pack('classifier'**, model)

  2. 셀을 실행하여 %%writefile keras_fashion_mnist_server.py 에서 keras_fashion_mnist_server.py 가 이미 생성되어 있을때, artifact 이름만 바꾸고 다시 실행하면 Overwrite 했다는 메시지가 나옵니다.
    하지만 실제로는 덮어쓰지 않아 artifact의 이름이 바뀌어 있지 않습니다.
    새로운 이름으로 writefile을 해주거나 기존 파일을 지우고 실행해주세요

  • API 설정
  @api(input=ImageInput(pilmode='L'), batch=True)  def predict(self, imgs: List[np.ndarray]) -> List[str]:

@api 데코레이터를 통해 inference API가 생성이 됩니다.
API 이름은 함수명으로 설정되며 여기서는 predict가 됩니다.
그리고 batch flag 가 True가 되면 여러 개의 인풋을 받아 여러 개의 결과를 반환할 수 있습니다.

  • 모델 추론
class_idxs = self.artifacts.classifier.predict_classes(inputs)

실제로 모델이 추론하는 부분입니다. 여기서 predict_classes는 keras model의 함수입니다.
즉 classifier가 MyModel의 model 을 가지고 있습니다.

해당 셀을 실행하면 %%writefile keras_fashion_mnist_server.py의 구문에 의해 keras_fashion_mnist_server.py 파일이 생성됩니다.
파일로 저장하는 이유는 추후에 있을 패키징작업에서 독립적으로 불러오기 위함입니다.

bentoml

bentoml 은 pip 패키지를 설치할 수 있는 옵션들도 제공합니다.
관련 API 및 고급기능은 BENTOML_API 를 참조하세요.


모델 API 서버 패키징하기#

KerasFashionMnistService를 이제 API 서버 컨테이너로 만들기 위해 패키징을 진행합니다.
KerasFashionMnistService가 상속받아 가지고 있는 BentoService 에서 제공하는 함수를 사용하여 진행합니다.

아래 코드는 아까 학습한 MyModel의 model을 KerasFashionMnistService에 등록하고 저장하는 코드입니다.

BentoML Service#

fashion_mnist_serving.ipynb
# fashion_mnist_serving.ipynbfrom keras_fashion_mnist_server import KerasFashionMnistService
fashion_mnist_svc = KerasFashionMnistService()fashion_mnist_svc.pack('classifier', model)
saved_path = fashion_mnist_svc.save(labels={"Validation-accucray":"89.12"})print(saved_path)

간단하게 코드에 사용된 bentoml 함수를 살펴보겠습니다.

pack
API 서비스 클래스에 모델을 저장합니다.

caution

bentoml의 최신 버전에서는 pack을 사용하지 않을 수 있습니다.
https://docs.bentoml.org/en/latest/api/bentoml.html?highlight=pack

save
BentoML 서버에 모델 서비스 클래스를 저장하고 등록합니다.
각종 메타데이터를 담고 있는 파일들과 모델파일, 컨테이너화를 위해 필요한 파일 등을 생성하여 저장합니다.
CAP 노트북서버에서는 /home/jovyan/bentoml 에 저장됩니다.

Parameters#

version(optional) : 서비스 버전을 입력합니다. 입력하지 않으면 {생성일자}_{해쉬코드} 로 자동생성됩니다.
labels(optional) : 서비스에 라벨을 지정할 수 있습니다. 아래와 같이 모델의 정확도 같은 걸 넣으시면 됩니다. fashion_mnist_svc.save(labels={"Validation-accucray":"89.12"})

셀을 하나 추가하고 위 코드를 붙여넣고 실행해봅시다.

save() 후 반환되는 경로를 통해서 생성된 모델 API 서버의 내용을 알 수 있습니다. /home/jovyan/bentoml/repository/KerasFashionMnistService/..
해당 경로로 찾아가서 보면 Dockerfile 및 각종 파일들을 생성되어 있는 것을 확인 할 수 있습니다.
!tree bentoml

bentoml_save bentoml_tree


모델 API 서버 컨테이너 이미지로 만들기#

fairing의 builder를 사용해서 모델 API 서버의 컨테이너 이미지를 만들어 보겠습니다.
먼저 building_serving_server.ipynb 라는 빈 노트북을 생성합니다.

이번 장에서 fairing은 학습을 수행하는 것이 아닌 컨테이너 이미지를 만드는 것이 목적입니다.
따라서 deployer를 제외한 preprocessor와 builder만 사용합니다.

심화

fairing의 append builder는 Dockerfile을 지원하지 않기 때문에
fairing 튜토리얼에서 소개한 cluster builder를 사용해서 컨테이너 이미지화를 진행합니다.

cluster builder는 컨테이너 이미지에 들어가는 내용들을 외부 저장소에 저장한 후, 저장된 내용을 기반으로
kaniko 컨테이너 이미지 빌더가 이미지를 생성해줍니다.
그리고 생성된 이미지는 미리 정의된 이미지 저장소로 push됩니다.

Object Stroage 만들기#

시작하기 앞서 외부 저장소인 Storage를 먼저 만들어 줍니다.

CAP Dashboard의 Storage 메뉴로 들어가 handson이라는 스토리지를 생성합니다.
이 스토리지에 cluster builder가 필요한 내용들을 저장하게 됩니다.
create-storage

cluster builder를 이용해 KerasFashionMnistService의 컨테이너를 이미지로 빌드하는는 순서는 이렇습니다.

  1. fashion_mnist_svc.save() 가 만들어진 파일들을 압축합니다. (preprocessor)
  2. 압축된 파일을 Storage에 저장합니다. (cluster builder)
  3. Kaniko image builder가 Storage에 저장된 압축된 파일을 가져와 이미지를 만듭니다. (cluster builder)
  4. 이미지 저장소(도커 허브 또는 CAP harbor)에 생성된 이미지를 푸시(push) 합니다. (cluster builder)

이 과정을 fairingBentomlProcessor preprocessorcluster builder에서 처리합니다.

preprocessor 정의하기#

아까 save() 하면서 나온 로그에서 힌트를 얻을 수 있습니다.
fashionmnist_svc.save()는 /home/jovyan/bentoml/repository/서비스 클래스명/서비스 버전_
형태로 폴더를 생성합니다. 여기서 서비스 태그는 생성일자_해시코드로 이루어져 있습니다.

하지만 bentoml은 cli client도 제공하기 때문에 이전 로그를 찾을 필요 없이 노트북에서

get_bento_list
!bentoml list

bentoml_list

로 확인이 가능합니다.

KerasFashionMnistService:20210723014132_8E9F10을 에서
KerasFashionMnistService를 service_name으로,
20210723014132_8E9F10을 service_tag로 정의하겠습니다.

important

!bentoml list 명령어로 얻은 여러분의 BENTO SERVICE 이름을 넣어주세요
KerasFashionMnistService:[생성일자]_[해시코드]

building_serving_server.ipynb
# building_serving_server.ipynbfrom kubeflow.fairing.preprocessors.bentoml import BentomlProcessorservice_name = "KerasFashionMnistService"service_tag = "20210723014132_8E9F10"preprocessor = BentomlProcessor(service_name=service_name, service_tag=service_tag)

Storage 정의하기#

cluster builder는 Storage에 압축된 파일을 저장소에 올린후, 이미지를 생성 하고 저장소에 push하는 역할을 합니다.
좀 전에 만든 스토리지를 사용하여 MinioContextSource 라는 클래스를 정의합니다.
preprocessor 가 파일을 압축하고 builder가 그 압축파일을 가져올 Storage를 설정합니다.

building_serving_server.ipynb
# building_serving_server.ipynbfrom kubeflow.fairing.builders.cluster.minio_context import MinioContextSourcestorage_endpoint = "https://www.gocap.kr:[port]" # Endpoint 주소를 적어주세요storage_access = "handson" # 자신의 Access Key 정보를 넣어주세요storage_key = "YOURKEY"  # 자신의 Secret Key 키 정보를 복사해서 넣어주세요region = "us-east-1"  # region은 아무 String이나 넣어도 상관 없습니다. 빈 값만 아니면 됩니다.myserver_source = MinioContextSource(endpoint_url=storage_endpoint,                                    minio_secret=storage_access,                                    minio_secret_key=storage_key,                                    region_name=region)

Storage의 endpoint, access, key 정보는 CAP Storage 화면으로 가셔서 직접 만드신
storage의 자물쇠 버튼을 누르시면 확인하실 수 있습니다.

storage_info_button
storage_info


컨테이너 이미지 정의하기 (TAG 지정하기)#

이전 장에서 fairing 을 이용한 학습이 진행되면 컨테이너 이미지 태그에 랜덤한 해시 값이 붙었습니다.
이 태그는 임시 값(랜덤값)이기에 학습된 모델의 버전관리가 힘들어 이번에는 직접 태그를 지정하겠습니다.

building_serving_server.ipynb
# building_serving_server.ipynbDOCKER_REGISTRY = "YOURID"# DOCKER_REGISTRY = "harbor.dudaji.com/프로젝트 이름image_name = "myserving-server"image_tag = "handson"

이렇게 설정하면 생성될 모델 API 서버의 이미지 명은 YOURID/myserving-server:handson이 됩니다.


Cluster Builder 정의하기#

Storage 설정이 끝났으니 cluster builder에 preprocesser 와 storage 설정값을 넣어줍니다.

building_serving_server.ipynb
# building_serving_server.ipynbfrom kubeflow.fairing.builders.cluster.cluster import ClusterBuilderbuilder = ClusterBuilder(registry=DOCKER_REGISTRY,                        image_name=image_name,                        tag=image_tag,                        preprocessor=preprocessor,                        context_source=myserver_source,                        push=True)image_name = builder.build()print(image_name)

여기서 push=True 는 컨테이너 이미지가 생성이 되면 바로 저장소로 push 할것인가에 대한 옵션입니다.
빌드가 완료되면 이미지 이름을 출력합니다. 아까 정한대로 YOURID/myserving-server:handson 이 출력될 것입니다.

아래는 전체 코드입니다.

building_serving_server.ipynb
# building_serving_server.ipynb 전체 코드from kubeflow.fairing.builders.cluster.cluster import ClusterBuilderfrom kubeflow.fairing.builders.cluster.minio_context import MinioContextSourcefrom kubeflow.fairing.preprocessors.bentoml import BentomlProcessor
service_name = "KerasFashionMnistService"service_tag = "YOURSERVICETAG"preprocessor = BentomlProcessor(service_name=service_name, service_tag=service_tag)
storage_endpoint = "YOURSTORAGEENDPOINT"storage_access = "YOURACCESSKEY"storage_key = "YOURSECRETKEY"region = "ANYSTRING"myserver_source = MinioContextSource(endpoint_url=storage_endpoint,                                      minio_secret=storage_access,                                      minio_secret_key=storage_key,                                      region_name=region)
DOCKER_REGISTRY = "YOURID"# DOCKER_REGISTRY = "harbor.dudaji.com/프로젝트 이름
image_name = "myserving-server"image_tag = "handson"
builder = ClusterBuilder(registry=DOCKER_REGISTRY,                        image_name=image_name,                        tag=image_tag,                        preprocessor=preprocessor,                        context_source=myserver_source,                        push=True)image_name = builder.build()print(image_name)

코드를 실행하여 컨테이너 이미지를 생성해봅시다. 클러스터에서 처음 빌드되는 것이기 때문에 시간이 조금 걸릴 것입니다.
그 다음 실행부턴 캐시를 이용하기 때문에 속도가 빨라집니다.

build_server build_server

Building image brightfly/myserving-server:handson done.

위 로그가 확인되면 정상적으로 컨테이너 이미지가 빌드된 것입니다.
이제 저 이미지 주소를 알고 있다면 어디서든 모델을 서비스하는 API 서버를 실행시킬 수 있게 되었습니다.


빌드된 모델 API 서버를 로컬 PC에서 실행해보기 (선택사항)#

important

로컬 피씨에 Docker가 설치되어 있어야 합니다.

터미널에서 아래와 같이 실행합니다.

docker_run
sudo docker run -p 5000:5000 YOURID/myserving-server:handson

local_run local_run

정상적으로 실행이 되면 브라우저를 열어 http://localhost:5000을 입력합니다.

06.model_swagger

model api server는 기본적으로 Swagger UI를 제공하기 때문에 테스트를 쉽게 진행할 수 있습니다.
그러면 이미지로 빌드 된 MyModel이 fashion mnist 데이터를 잘 분류하는지 테스트 해보겠습니다.

화면내 app의 /predict를 선택해서 Try it out 버튼을 누릅니다. 그러면 파일을 업로드 할 수 있는 버튼이 생깁니다.

06.app_try_it_out

기존 test dataset서 가져온 이미지를 넣어 봅시다. 아래 이미지를 저장하여 입력해봅시다.
이 이미지는 Sneaker로 라벨링되어 있습니다.

06.sneaker_test

app_predict

🎉 🎉 잘 작동합니다.


모델 API 서버 배포하기#

API 서버로 만든 모델 서비스를 클러스터에 배포 해보겠습니다.
배포는 fairing의 deployer중 BentomlServing를 이용하여 진행합니다.

먼저 deploy_serving_server.ipynb 라는 새 노트북을 생성합니다.
준비해야 할 것은 모델 API 서버의 컨테이너 이미지 이름과 서비스 이름, 서비스 태그 입니다.

deploy_serving_server.ipynb
# deploy_serving_server.ipynbfrom kubeflow.fairing.deployers.bentoml.serving import BentomlServing
serving_image = "YOURID/myserving-server:handson"# serving_image = "harbor.dudaji.com/프로젝트 이름/myserving-server:handson"service_name = "KerasFashionMnistService"service_tag = "YOURTAG"
serving = BentomlServing(serving_image=serving_image,                        service_tag=service_tag,                        service_name=service_name)serving.deploy()
bentomlserving

BentomlServing은 전달받은 컨테이너 이미지를 클러스터에서 실행시켜 모델 API 서버를 띄우고
외부에서 접속할 수 있는 포트를 연결시켜줍니다.

serving_deploy

정상적으로 배포되면 포트번호가 출력됩니다.
브라우저에서 현재 주소에서 출력된 포트만 입력해서 API 서버를 열어봅시다.
BentomlServing은 API 서버를 관리하기 위해 몇가지 함수를 제공합니다

  • deploy() : API 서버를 클러스터에 실행합니다.
  • delete() : 현재 실행되고 있는 API 서버를 제거합니다.
  • get_service_port() : API 서버의 서비스 포트를 반환합니다.
  • go_swagger() : API 서버의 swagger UI를 새 브라우저에서 호출합니다. 노트북에서만 가능합니다.

모델 API 서버 테스트해보기#

배포된 모델 API 서버를 브라우저상에서 확인한 후 테스트 해보겠습니다.
이번에는 swagger UI 대신 curl 을 이용해 테스트 해보겠습니다.
아래의 이미지를 저장해서 sample_image.png 란 이름으로 노트북서버에 올려봅시다.

이미지 올리기

노트북서버의 사이드바에 파일을 끌어서 내려놓으면(DRAG&DROP) 노트북서버에 파일이 올라갑니다.

이 이미지는 Ankle boot입니다.

sample_image

이제 노트북에서 셀 또는 터미널을 열고 테스트 해봅시다.

!curl -X POST "http://YOUR-ENDPOINT/predict" -H "Content-Type: image/png" --data-binary @sample_image.png
"Ankle boot"

이제 우리는 MyModel을 모델을 서비스하는 API 서버까지 배포할 수 있게 됐습니다.