대부분의 모델 서버는 텐서를 입력 데이터로 이용합니다. 다시 말해서, 모델을 학습할 때 사용한 데이터의 형태로 이용하는 것입니다. 이 데이터들은 효율적인 학습을 위해서 사전 처리 단계를 거칩니다. 그래서 원시 데이터와는 다릅니다. 예를 들어서 원시 데이터를 이미지로 사용한다고 가정하면, 학습을 위해서 이미지의 크기를 조절하고, 각 필셀 값의 범위를 조정합니다. 그래서 모델 서버에 예측을 요청할 때도 사전 처리를 거친 데이터를 입력해야만 정상적인 결과가 나오게 되는 것입니다. 즉 사용자가 원시 입력 형식으로 전송하는 경우 예측 요청을 하기 전에 사전 처리 단계가 필요합니다. 이럴 경우 Transformer를 사용할 수 있습니다.
Transformer를 이용하면, 사전/사후 처리 코드를 사용자가 구현할 수 있습니다. 앞서 구현한 파이토치 예제에서는 텐서를 입력 받아서 예측을 하였습니다. 이 예제에 전처리 단계를 추가한 Transformer를 사용하여, 사용자가 원시 이미지 데이터를 보내 예측을 할 수 있도록 하겠습니다.
Transformer 이미지 생성하기
사전/사후 처리를 할 Transformer 이미지를 생성해 보겠습니다.
사전/사후 처리 코드 작성하기
먼저 사전 처리에서 사용할 image_transform() 메소드를 구현합니다. 사용자가 보내온 BASE64 로 인코딩된 원시 이미지 데이터를 텐서로 변환합니다. transforms.Compose()를 이용하여 이미지 크기도 조절하고, 정규화도 시킵니다.
사후 처리에서 사용할 top_5() 메소드를 구현합니다. 이미지 분류값을 사람이 보기 쉽게 레이블을 붙이주고, TOP 5 로 정리해서 보여줍니다.
kfserving.KFModel 를 상속 받아서 preprocess() 와 postprocess() 메소드를 구현해 줍니다.
image_transformer.py
import argparse import base64 import io import logging from typing import Dict import kfserving import numpy as np import torch import torchvision.transforms as transforms from PIL import Image logging.basicConfig(level=kfserving.constants.KFSERVING_LOGLEVEL) transform = transforms.Compose([ transforms.Resize(32), transforms.CenterCrop(32), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) def image_transform(instance): byte_array = base64.b64decode(instance['image']['b64']) image = Image.open(io.BytesIO(byte_array)) im = Image.fromarray(np.asarray(image)) res = transform(im) logging.info(res) return res.tolist() CLASSES = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'] def top_5(prediction): pred = torch.as_tensor(prediction) scores = torch.nn.functional.softmax(pred, dim=0) _, top_5 = torch.topk(pred, 5) results = {} for idx in top_5: results[CLASSES[idx]] = scores[idx].item() return results class ImageTransformer(kfserving.KFModel): def __init__(self, name: str, predictor_host: str): super().__init__(name) self.predictor_host = predictor_host def preprocess(self, inputs: Dict) -> Dict: return {'instances': [image_transform(instance) for instance in inputs['instances']]} def postprocess(self, inputs: Dict) -> Dict: return {'predictions': [top_5(prediction) for prediction in inputs['predictions']]} if __name__ == "__main__": DEFAULT_MODEL_NAME = "model" parser = argparse.ArgumentParser(parents=[kfserving.kfserver.parser]) parser.add_argument('--model_name', default=DEFAULT_MODEL_NAME, help='The name that the model is served under.') parser.add_argument('--predictor_host', help='The URL for the model predict function', required=True) args, _ = parser.parse_known_args() transformer = ImageTransformer(args.model_name, predictor_host=args.predictor_host) kfserver = kfserving.KFServer() kfserver.start(models=[transformer])
컨테이너 이미지를 만들기
컨테이너 이미지를 만들기 위한 Dockerfile 입니다. 파이썬을 기본 이미지로 사용하고, torch
와 torchvision
패키지를 추가로 설치합니다.
ENTRYPOINT를 이용하여 image_transformer.py
를 실행합니다. Transformer 컨테이너가 실행 될때 --model_name
과 --predictor_host
같은 인자 값들이 넘어오기 때문에, 꼭 ENTRYPOINT를 사용해야합니다.
Dockerfile
FROM python:3.6-slim RUN pip install torch torchvision RUn pip install kfserving==0.3.0 image numpy ENV APP_HOME /app WORKDIR $APP_HOME ADD image_transformer.py /app/ ENTRYPOINT ["python", "image_transformer.py"]
다음 명령어를 실행하면 kangwoo/kfserving-transformer:0.0.1 라는 이름의 컨테이너 이미지를 빌드 할 수 있습니다.
docker build -t kangwoo/kfserving-transformer:0.0.1 .
빌드한 컨테이너 이미지를 컨테이너 레지스트리로 업로드 하겠습니다.
docker push kangwoo/kfserving-transformer:0.0.1
Transoformer를 포함한 InferenceService 로 예측 하기
InferenceService 생성
InferenceService 매니페스트를 작성합니다. 예측을 위한 predictor
는 파이토치에서 사용한 것을 그대로 재사용하겠습니다. transformer
필드를 이용하여서 생성한 Transformer 이미지를 지정해 줍니다.
transformer.yaml
apiVersion: "serving.kubeflow.org/v1alpha2" kind: "InferenceService" metadata: name: "pytorch-cifar10-transformer" spec: default: predictor: pytorch: storageUri: "pvc://kfserving-models-pvc/models/pytorch/cifar10/" modelClassName: "Net" transformer: custom: container: image: kangwoo/kfserving-transformer:0.0.1
InferenceService 를 생성합니다.
다음은 admin 네임스페이스 InferenceService 를 생성하는 예제입니다.
kubectl -n admin apply -f transformer.yaml
생성한 InferenceService를 조회해 보겠습니다.
kubectl -n admin get inferenceservice
InferenceService 가 정상적으로 생성되면 다음과 같은 응답 결과를 확인할 수 있습니다.
NAME URL READY DEFAULT TRAFFIC CANARY TRAFFIC AGE pytorch-cifar10-transformer <http://pytorch-cifar10-transformer.admin.example.com/v1/models/pytorch-cifar10-transformer> True 100 21s
예측 실행하기
예측을 요청하기 위해서는 모델 서버에 접근해야 합니다. 모델 서버는 ingressgateway 를 통해서 접근할 수 있습니다. ingressgateway 는 모델 서버들을 구분하기 위해서 호스트 이름을 사용합니다. ingressgateway에 접근하 기 위한 주소는 앞서 정의한 CLUSTER_IP 를 사용하겠습니다.
예측을 요청할 데이터를 json 파일로 작성합니다.
데이터의 크기가 크기 때문에 git 에 있는 파일을 다운받아서 사용해주세요.
input.json
{ "instances": [ { "image": { "b64": "..." } } ] }
b64
필드에는 BASE64로 인코딩된 이미지가 들어 있습니다. 만약 다른 이미지로 테스트해 보고 싶다면, 다음 코드를 참조하여 값을 변경하면 됩니다.
import base64 with open("airplane.jpg", "rb") as image_file: encoded_string = base64.b64encode(image_file.read()) print(encoded_string.decode())
다음은 admin 네임스페이스의 pytorch-cifar10-transformer InferenceService 에 예측을 요청하는 예제입니다.
MODEL_NAME=pytorch-cifar10-transformer SERVICE_HOSTNAME=$(kubectl -n admin get inferenceservice pytorch-cifar10-transformer -o jsonpath='{.status.url}' | cut -d "/" -f 3) INPUT_PATH=@./input.json curl -v -H "Host: ${SERVICE_HOSTNAME}" http://$CLUSTER_IP/v1/models/$MODEL_NAME:predict -d $INPUT_PATH
정상적으로 실행되면 다음과 같은 응답 결과를 확인 할 수 있습니다.
* Trying 192.168.21.38... * TCP_NODELAY set * Connected to 192.168.21.38 (192.168.21.38) port 32380 (#0) > POST /v1/models/pytorch-cifar10-transformer:predict HTTP/1.1 > Host: pytorch-cifar10-transformer.admin.example.com > User-Agent: curl/7.64.1 > Accept: */* > Content-Length: 4551 > Content-Type: application/x-www-form-urlencoded > Expect: 100-continue > < HTTP/1.1 100 Continue * We are completely uploaded and fine < HTTP/1.1 200 OK < content-length: 176 < content-type: application/json; charset=UTF-8 < date: Sat, 04 Apr 2020 09:43:33 GMT < server: istio-envoy < x-envoy-upstream-service-time: 45 < * Connection #0 to host 192.168.21.38 left intact {"predictions": [{"airplane": 0.9271695017814636, "ship": 0.06180185452103615, "bird": 0.004641484934836626, "deer": 0.003963686991482973, "automobile": 0.000884002773091197}]} * Closing connection 0