Kubeflow KFServingでscikit-learnの学習済みモデルを用いた推論サービスを公開する

著者: 株式会社 日立ソリューションズ 柳村 明宏
監修: 株式会社 日立製作所

# はじめに
[Kubeflow](https://www.kubeflow.org/)はKubernetes上でMLOpsを実現するためのOSSのツールキットです。2020/3にバージョン1.0がリリースされ、2021/4現在も活発に開発が行われており、機械学習を利用したシステムの開発/運用のライフサイクルを回すための有効な手段の１つとして注目されています。

本連載では、Kubeflowのバージョン1.2(連載開始時点の最新版、2020年11月リリース)について、構築手順、Kubeflow Pipelines、KFServingの基礎的な利用手順についての情報を紹介いたします。

第4回の本稿では、[第3回⽬](https://qiita.com/t-takahisa/items/0a409478cb502f4d3c5f)で作成した学習済みモデルを利⽤した推論サービスを、Kubeflowのサービングコンポーネントの１つであるKFServingを利用して公開する⼿順を紹介いたします。

本連載では、Kubernetesの前提バージョンを1.20としています。
ステータスがStableのSeldon Core Servingは、前提とするKubernetesのバージョンが1.12から1.17であったため、ステータスがBetaであるKFservingを利用した検証を行いました。



投稿一覧：
1. [MLOpsを実現するKubeflowを前提のKubernetesも含めて構築する(前編)](https://qiita.com/t-takahisa/items/5eefa9cf6be783cfe170)
2. [MLOpsを実現するKubeflowを前提のKubernetesも含めて構築する(後編)](https://qiita.com/t-takahisa/items/c481cdb188096eb29289)
3. [Kubeflow Pipelinesでscikit-learnの機械学習モデルを訓練・評価してみる](https://qiita.com/t-takahisa/items/0a409478cb502f4d3c5f)
4. Kubeflow KFServingでscikit-learnの学習済みモデルを用いた推論サービスを公開する (本投稿)


# KFServingの概要
KFServingは、Kubernetes上でのサーバレス推論を実現するコンポーネントで、TensorFlow、XGBoost、Scikit-learn、PyTorch、ONNXなどの一般的な機械学習（ML）フレームワークを抽象度の高いYAMLファイルに記述することで構築することができます。
KFServingの詳細は、Kubeflow公式サイトの[KFServingのページ](https://www.kubeflow.org/docs/components/kfserving/kfserving/)を参照してください。

# KFServingの利用手順
今回のKFServingの利用手順を次に示します。

**今回のKubeflow Pipelinesの利用手順**

| # | 手順 | 概要 |
| :- | :- | :- |
| 1 | KFServingの開発環境を準備 | [第1回目](https://qiita.com/t-takahisa/items/5eefa9cf6be783cfe170)、および、[第2回目](https://qiita.com/t-takahisa/items/c481cdb188096eb29289)でkubeflowを構築したマシンにログインし、KFServingのリポジトリをクローンします。|
| 2 | 推論サービスのデプロイ | scikit-learnのsampleを編集して、[第3回目](https://qiita.com/t-takahisa/items/0a409478cb502f4d3c5f)でAmazon S3にアップロードしたscikit-learnの学習済みモデルの読込み、および、推論処理を行えるようにします。編集したコードのDockerイメージをビルドし、推論サービスのデプロイを行います。 |
| 3 | 推論サービスの利用 | デプロイした推論サービスを利用し、推論を実行します。|


## 1. KFServingの開発環境を準備
### 1.1 SSHでログイン
[第1回目](https://qiita.com/t-takahisa/items/5eefa9cf6be783cfe170)、および、[第2回目](https://qiita.com/t-takahisa/items/c481cdb188096eb29289)で、Kubeflowを構築したマシンにSSHでログインします。

### 1.2 KFServingのバージョン確認
本稿では、Kubeflow構築済みの環境を前提とするため、KFServingのインストールは不要です。
また、ネームスペースはkubeflowとなります。
KFServingのバージョンは、「2.3 Amazon S3アクセスに必要なAWS認証設定」で編集する、KFServingのAWS認証用の設定ファイルの内容に影響します。

```shell
# KFServingのバージョン確認
kubectl get po -n kubeflow -o yaml | grep image.*kfserving-controller:
# --- 実行結果例 ここから -------------------------------------------------
#      image: gcr.io/kfserving/kfserving-controller:v0.4.1
#      image: gcr.io/kfserving/kfserving-controller:v0.4.1
# --- 実行結果例 ここまで -------------------------------------------------
```

### 1.3 KFServingのリポジトリのクローン
KFServingのGitHubのリポジトリをクローンします。

```shell
# KFServingのリポジトリをクローン
git clone https://github.com/kubeflow/kfserving.git
```

scikit-learnのsampleは、次のサンプルを利用します。
[YAMLファイルのsample](https://github.com/kubeflow/kfserving/tree/master/docs/samples/v1alpha2/sklearn)
[コードのsample](https://github.com/kubeflow/kfserving/tree/master/python/sklearnserver)

編集するファイルは次のファイルです。

| # | 記載箇所 | ファイルパス |
| :- | :- | :- |
| 1 | 2.1 scikit-learnの学習済みモデルを用いた推論処理を作成 | ~/kfserving/python/sklearnserver/sklearnserver/model.py |
| 2 | 2.2 Dockerイメージのビルド | ~/kfserving/python/sklearnserver/setup.py |
| 3 | 2.3 Amazon S3アクセスに必要なAWS認証設定 | ~/kfserving/docs/samples/storage/s3/s3_secret.yaml |
| 4 | 2.4 推論サービスのデプロイ | ~/kfserving/docs/samples/v1alpha2/sklearn/sklearn.yaml |

## 2.推論サービスのデプロイ
### 2.1 scikit-learnの学習済みモデルを用いた推論処理を作成
[第3回目](https://qiita.com/t-takahisa/items/0a409478cb502f4d3c5f)でAmazon S3にアップロードしたscikit-learnの学習済みモデルを読込み、KFServingを利用して推論を実行できるように「~/kfserving/python/sklearnserver/sklearnserver/model.py」を編集します。
model.pyの編集後のコードを次に示します。編集前のコード(KFServingのscikit-learnサンプルのmodel.py)については、[model.pyのsample](https://github.com/kubeflow/kfserving/blob/master/python/sklearnserver/sklearnserver/model.py)を参照してください。

```python:model.py
import kfserving
import os
import pickle
import pandas as pd
import sys
import json
from typing import Dict
sys.path.append('/sklearnserver/sklearnserver')
from preprocessor import PreProcessor

MODEL_BASENAME = "ml_pipeline"
MODEL_EXTENSIONS = [".pickle"]

class SKLearnModel(kfserving.KFModel):  # pylint:disable=c-extension-no-member
    def __init__(self, name: str, model_dir: str):
        super().__init__(name)
        self.name = name
        self.model_dir = model_dir
        self.ready = False

    # モデルファイルの読込み処理
    def load(self) -> bool:
    model_path = kfserving.Storage.download(self.model_dir)
         paths = [os.path.join(model_path, MODEL_BASENAME + model_extension)
                 for model_extension in MODEL_EXTENSIONS]
        for path in paths:
            if os.path.exists(path):
                self._model = pickle.load(open(path, 'rb'))
                self.ready = True
                break
        return self.ready

    # 推論処理
    def predict(self, request: Dict) -> Dict:
        # リクエストから値を取得
        instances = request["instances"]
        try:
            # DataFrame化
            X_df = pd.DataFrame(instances)
            # 予測を実行
            pred = self._model.predict(X_df)
            # 予測結果をJson化
            result = {"Survived": int(pred[0])}
            # 予測結果を返信
            return json.dumps(result)
        except Exception as e:
            raise Exception("Failed to predict %s" % e)

```
MODEL_BASENAME、MODEL_EXTENSIONSは、[第3回目](https://qiita.com/t-takahisa/items/0a409478cb502f4d3c5f)で作成した学習済みモデルを指定します。

model.pyでは、[第3回目](https://qiita.com/t-takahisa/items/0a409478cb502f4d3c5f)で作成したpreprocessor.pyをインポートするため、preprocessor.pyを「~/kfserving/python/sklearnserver/sklearnserver/」に配置してください。

### 2.2 Dockerイメージのビルド
「2.1 scikit-learnの学習済みモデルを用いた推論処理を作成」で編集したコードをビルドしてDockerイメージを作成するために、「~/kfserving/python/sklearnserver/setup.py」を編集します。
setup.pyの編集後のコードを次に示します。ここでは次の編集を実施します。

- 「install_requires=」のscikit-learnのバージョンを、学習済モデルを作成したときに利用したscikit-learnのバージョンに合わせます。
- 追加したいライブラリがあれば追加で記載します。今回は「pandas >= 1.0.0"」を記載しています。
編集前のコード(ライブラリ配布用のpythonサンプルのsetup.py)については、[setup.pyのsample](https://github.com/kubeflow/kfserving/blob/aa1042a3b6f72a65f16e13771ce335b1844ad7fc/python/sklearnserver/setup.py)を参照してください。

```python:setup.py
from setuptools import setup, find_packages

tests_require = [
    'pytest',
    'pytest-asyncio',
    'pytest-tornasync',
    'mypy'
]
setup(
    name='sklearnserver',
    version='0.5.0',
    author_email='singhan@us.ibm.com',
    license='https://github.com/kubeflow/kfserving/LICENSE',
    url='https://github.com/kubeflow/kfserving/python/sklearnserver',
    description='Model Server implementation for scikit-learn. \
                 Not intended for use outside KFServing Frameworks Images',
    long_description=open('README.md').read(),
    python_requires='>3.4',
    packages=find_packages("sklearnserver"),
    install_requires=[
        "kfserving>=0.5.0",
        "scikit-learn == 0.24.1",
        "pandas >= 1.0.0"
    ],

    tests_require=tests_require,
    extras_require={'test': tests_require}
)
```
インストールするscikit-learnのバージョンは、[第3回目](https://qiita.com/t-takahisa/items/0a409478cb502f4d3c5f)で指定したバージョンと同一のバージョンを指定します。

自身のDockerレジストリにログインします。

```shell
# docker loginの実行
sudo docker login
```

Dockerイメージを作成します。
参考: [Building your own scikit-Learn Server Docker Image](https://github.com/kubeflow/kfserving/tree/aa1042a3b6f72a65f16e13771ce335b1844ad7fc/python/sklearnserver#building-your-own-scikit-learn-server-docker-image)

```shell
# Dockerイメージの作成
sudo docker build -t [Dockerユーザ名]/[イメージ名] -f ~/kfserving/python/sklearn.Dockerfile ~/kfserving/python
# --- 実行結果例 ここから -------------------------------------------------------------------------------
# Successfully built 81d6efc54e84
# Successfully tagged [Dockerユーザ名]/[イメージ名]:latest
# --- 実行結果例 ここまで -------------------------------------------------------------------------------

# 作成したイメージの確認
sudo docker images --digests [Dockerユーザ名]/[イメージ名]:latest
# --- 実行結果例 ここから -------------------------------------------------------------------------------
# REPOSITORY                     TAG       DIGEST        IMAGE ID             CREATED            SIZE
# [Dockerユーザ名]/[イメージ名]   latest     <none>       41909b0f94e0        42 minutes ago      1.36GB
# --- 実行結果例 ここまで -------------------------------------------------------------------------------

# 作成したイメージのアップロード
sudo docker push [Dockerユーザ名]/[イメージ名]:latest
# --- 実行結果例 ここから -------------------------------------------------------------------------------
# The push refers to repository [docker.io/[Dockerユーザ名]/[イメージ名]]
# 9ef05367b9dc: Pushed
# b84a57dacdd2: Pushed
# 1add26dfd9ff: Pushed
# 9df36e5bd672: Pushed
# 16be529e8612: Pushed
# 9ab20c4343be: Layer already exists
# 7ebccfde7bb7: Layer already exists
# d20676915161: Layer already exists
# 852a1bb5386c: Layer already exists
# 9437609235f0: Layer already exists
# bee1c15bf7e8: Layer already exists
# 423d63eb4a27: Layer already exists
# 7f9bf938b053: Layer already exists
# f2b4f0674ba3: Layer already exists
# latest: digest: sha256:66dbf7d11e89d824b8e48f9e568ecd0049f21e2b76f694f5cb3c68365afa5ad0 size: 3272 
# --- 実行結果例 ここまで -------------------------------------------------------------------------------
```

Inference Service用のconfigmapに、作成したscikit-learn のDockerイメージを指定します。
参考: [Run SKLearn InferenceService with your own image](https://github.com/kubeflow/kfserving/tree/aa1042a3b6f72a65f16e13771ce335b1844ad7fc/docs/samples/v1alpha2/sklearn#run-sklearn-inferenceservice-with-your-own-image)

```shell
# Inference Service用のconfigmapの編集
kubectl -n kubeflow edit configmap inferenceservice-config
```
```config:configmap
：（省略）
  predictors: |-
    {
：（省略）
        "sklearn": {
            "image": "[Dockerユーザ名]/[イメージ名]"
        },
：（省略）

```

### 2.3 Amazon S3アクセスに必要なAWS認証設定
Amazon S3に格納しているモデルファイルを参照するため、AWS認証用のYAMLファイル「~/kfserving/docs/samples/storage/s3/s3_secret.yaml」について、公式ドキュメントを参考に編集します。
参考: [s3_secret.yaml](https://github.com/kubeflow/kfserving/tree/aa1042a3b6f72a65f16e13771ce335b1844ad7fc/docs/samples/storage/s3)、[Store models in S3 bucket](https://www.kubeflow.org/docs/aws/aws-e2e/#store-models-in-s3-bucket)

今回、KFServingは0.41を利用しているため、0.4以降のバージョンを利用する場合の例を記載します。
0.3以前のバージョンを利用する場合は、dataタグに指定するAWS_ACCESS_KEY_ID、および、AWS_SECRET_ACCESS_KEYの変数名をそれぞれ、awsAccessKeyID、および、awsSecretAccessKeyと指定します。
AWS_ACCESS_KEY_ID、および、AWS_SECRET_ACCESS_KEYの値をBASE64エンコードして設定
<…の値>部分は環境に合わせて指定します。

```shell
export AWS_ACCESS_KEY_ID_BASE64=`echo -n "<AWS_ACCESS_KEY_IDの値>" | base64`
export AWS_SECRET_ACCESS_KEY_BASE64=`echo -n "<AWS_SECRET_ACCESS_KEYの値>" | base64`
```

```yaml:s3_secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: mysecret
  annotations:
   serving.kubeflow.org/s3-endpoint: s3.ap-northeast-1.amazonaws.com
   serving.kubeflow.org/s3-usehttps: "1"
   serving.kubeflow.org/s3-verifyssl: "1"
   serving.kubeflow.org/s3-region: ap-northeast-1
type: Opaque
data:
  AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID_BASE64}
  AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY_BASE64}
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sa
secrets:
  - name: mysecret
```
s3-endpoint、および、s3-regionは環境に合わせて設定します。

AWS認証情報をKubernetesへ設定します。

```shell
# AWS認証情報のKubernetesへの設定
kubectl apply -f s3_secret.yaml
# --- 実行結果例 ここから -----------------
# secret/mysecret created
# serviceaccount/sa created
# --- 実行結果例 ここまで -----------------
```

### 2.4 推論サービスのデプロイ
scikit-learn Inference Service用のYAML「~/kfserving/docs/samples/v1alpha2/sklearn/sklearn.yaml」を編集し、推論サービスをデプロイします。
sampleでは、storageUriにGCPのパスが記載されていますが、今回Amazon S3を利用するため、Amazon S3のパスを記載します。
参考: [Run SKLearn InferenceService with your own image](https://github.com/kubeflow/kfserving/tree/aa1042a3b6f72a65f16e13771ce335b1844ad7fc/docs/samples/v1alpha2/sklearn#run-sklearn-inferenceservice-with-your-own-image)

```yaml:sklearn.yaml
apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "sklearn-iris"
spec:
  default:
    predictor:
      serviceAccountName: sa
      sklearn:
        storageUri: "s3://kubeflow-pl"
```
serviceAccountNameには、「2.3 Amazon S3アクセスに必要なAWS認証設定」で編集したYAMLファイルのServiceAccountのnameを指定します。
storageUriには、[第3回目](https://qiita.com/t-takahisa/items/0a409478cb502f4d3c5f)で学習済みモデルをアップロードしたAmazon S3のUriを指定します。

scikit-learn Inference Serviceをデプロイします。

```shell
kubectl apply -f sklearn.yaml
# --- 実行結果例 ここから -------------------------------------------------------------------------------
# inferenceservice.serving.kubeflow.org/sklearn-iris created
# --- 実行結果例 ここまで -------------------------------------------------------------------------------

# Podの状態確認
kubectl get pods -l model=sklearn-iris
# --- 実行結果例 ここから -------------------------------------------------------------------------------
# NAME                                                      　　   READY   STATUS    RESTARTS   AGE
# sklearn-iris-predictor-default-6lvmc-deployment-856956bb95sgd5r   2/2　  Running    0         89s
# --- 実行結果例 ここまで -------------------------------------------------------------------------------

# ログの確認
kubectl logs sklearn-iris-predictor-default-6lvmc-deployment-856956bb95sgd5r kfserving-container
# --- 実行結果例 ここから -------------------------------------------------------------------------------
# [I 210310 16:28:10 storage:45] Copying contents of /mnt/models to local
# [I 210310 16:28:10 kfserver:115] Registering model: sklearn-iris
# [I 210310 16:28:10 kfserver:96] Listening on port 8080
# [I 210310 16:28:10 kfserver:98] Will fork 1 workers
# --- 実行結果例 ここまで -------------------------------------------------------------------------------
```

## 3. 推論サービスの利用
推論したいデータをJsonファイルで作成して、推論を実行します。
参考: [Run a prediction](https://github.com/kubeflow/kfserving/tree/aa1042a3b6f72a65f16e13771ce335b1844ad7fc/docs/samples/v1alpha2/sklearn#run-a-prediction)
正常に動作した場合、「2.1 scikit-learnの学習済みモデルを用いた推論処理を作成」で実装した通り、「{"Survived": 0}または{"Survived": 1}」が返ってきます。

### 3.1 入力データの準備
推論したいデータをJsonファイルで作成します。

```shell
# 推論データの作成
cat <<EOF> ml-input.json
{
  "instances": [
    {
      "PassengerId": 294,
      "Pclass": 1,
      "Name": "John, Mr. Smith",
      "Sex": "male",
      "Age": 28,
      "SibSp": 0,
      "Parch": 0,
      "Ticket": 312453,
      "Fare": 18.2500,
      "Cabin": "NaN",
      "Embarked": "Q"
    }
  ]
}
EOF
```

### 3.2 推論を実行
推論実行時に必要な環境変数を設定します。
MODEL_NAMEは、「2.4 推論サービスのデプロイ」で設定したmetadataタグのnameを指定してします。

```shell
MODEL_NAME=sklearn-iris
INPUT_PATH=ml-input.json
SERVICE_HOSTNAME=$(kubectl get inferenceservice sklearn-iris -o jsonpath='{.status.url}' | cut -d "/" -f 3)
export INGRESS_HOST=$(kubectl get po -l istio=ingressgateway -n istio-system -o jsonpath='{.items[0].status.hostIP}')
export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}')
```

KFServingのエンドポイントにHTTPリクエストを送信して、推論サービスを呼び出します。

```shell
# 推論の実行
curl -v -H "Host: ${SERVICE_HOSTNAME}" http://${INGRESS_HOST}:${INGRESS_PORT}/v1/models/$MODEL_NAME:predict -d $INPUT_PATH
# --- 実行結果例 ここから -------------------------------------------------------------------------------
# *   Trying [INGRESS_HOSTの値]...
# * TCP_NODELAY set
# * Connected to [INGRESS_HOSTの値] ([INGRESS_HOSTの値]) port [INGRESS_PORTの値] (#0)
# > POST /v1/models/sklearn-iris:predict HTTP/1.1
# > Host: sklearn-iris.default.example.com
# > User-Agent: curl/7.58.0
# > Accept: */*
# > Content-Length: 263
# > Content-Type: application/x-www-form-urlencoded
# >
# * upload completely sent off: 263 out of 263 bytes
# < HTTP/1.1 200 OK
# < content-length: 15
# < content-type: text/html; charset=UTF-8
# < date: Wed, 10 Mar 2021 16:36:26 GMT
# < server: istio-envoy
# < x-envoy-upstream-service-time: 26
# <
# * Connection #0 to host [INGRESS_HOSTの値] left intact
# {"Survived": 0}
# --- 実行結果例 ここまで -------------------------------------------------------------------------------
```

Podには下記のようにHTTPリクエストの結果が出力されます。

```shell
# ログの確認
kubectl logs sklearn-iris-predictor-default-6lvmc-deployment-856956bb95sgd5r kfserving-container
# --- 実行結果例 ここから -------------------------------------------------------------------------------
# [I 210310 16:36:26 web:2243] 200 POST /v1/models/sklearn-iris:predict (127.0.0.1) 22.87ms
# --- 実行結果例 ここまで -------------------------------------------------------------------------------
```

# おわりに
本稿では、Kubeflow KFServingでscikit-learnの学習済みモデルを用いた推論サービスを公開する手順を紹介しました。
Kubeflowの公式サイトなどに掲載されている情報やサンプルの多くは、Google Cloud Platform前提です。本投稿ではオンプレミスやGoogle Cloud Platform以外のクラウド環境で利用する方にも、基礎的な情報として参考してもらえればと思います。