BentoMLについて

# はじめに
機械学習プロジェクトで学習フェーズが完了すると、そのモデルを活用するフェーズに移ります。ローカルPC上で手動で推論するのであれば悩む事は無いですが、システムとして組み込むとなると、サービス提供のアーキテクチャを考える必要があります。そこでBentoMLというModel ServingのOSSに注目してみました。

https://docs.bentoml.org/en/latest/index.html

# BentoMLとは
[BentoML](https://github.com/bentoml/BentoML)は機械学習モデルを管理・提供するためのフレームワークです。様々なMLフレームワークで生成されたモデルを、統一的な手順・インターフェースによって提供するもので、モデル学習後から推論機能の提供までの準備や手続きを簡単にしてくれます。

また関連機能として、Kubernetes環境へのデプロイをサポートする[Yatai](https://github.com/bentoml/Yatai)、AWS、Azure、GCP等のクラウド環境へのデプロイをサポートする[bentoctl](https://github.com/bentoml/bentoctl)というOSSも合わせて公開されています。

今回はBentoMLの基本的な機能を中心に整理しようと思います。

# 前提条件とインストール方法
BentoMLはLinux/UNIX、Windows、MacOSをサポートしています。前提条件として Python 3.7 以上が必要となります。また、今回の話ではDockerも利用します。

インストールはpipで行います。
```sh:基本機能のインストール
pip install bentoml
```
```sh:追加的な機能（gRPC対応、S3サポートなど）も含めてインストールする場合
pip install "bentoml[all]"
```

# 基本的な作業プロセス
大まかなプロセスとしては以下のようになります。

#### Step1. 学習済モデルの管理
学習が完了したモデルをMentoMLモデルストアに格納します。

#### Step2. 推論サービスの作成
外部からAPIとして呼び出すことのできる推論サービスをプログラミングします。

#### Step3. ビルド
学習済モデル、推論サービスプログラム、その他必要な設定ファイルやデータファイルを取り纏めて1つのアーカイブファイルを作成します。このアーカイブを`Bento`と言います。

#### Step4. デプロイ
BentoからDockerイメージを生成してDockerコンテナとしてデプロイします。それ以外にも、Kubernetesやクラウドへのデプロイもサポートしています。

以下では公式ドキュメントにある[チュートリアル](https://docs.bentoml.org/en/latest/tutorial.html)をベースに各ステップの内容を整理しています。

## Step1. 学習済モデルの管理
![save_model.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/98ae00df-7902-b2fc-476f-bca3a56b6a2c.png)


BentoMLでは、まず、学習済モデルを`BentoML model store`に保存します。保存は`save_model`メソッドで行います。

```python:学習済モデルの保存
import bentoml

from sklearn import svm
from sklearn import datasets

# 学習データセットのロード
iris = datasets.load_iris()
X, y = iris.data, iris.target

# モデルの学習
clf = svm.SVC(gamma='scale')
clf.fit(X, y)

# BentoML model storeへの保存
saved_model = bentoml.sklearn.save_model("iris_clf", clf)
print(f"Model saved: {saved_model}")
```

上記の例では、モデル名"iris_clf"とscikit-learnで学習されたモデルを渡しています。保存したモデルにはバージョン識別のためのモデルタグ(例: `iris_clf:2uo5fkgxj27exuqj`)が自動的に割り振られます。このタグはBentoML model storeに格納された学習済モデルを操作する際の識別子になります。  
また最新バージョンのモデルに対しては`iris_clf:latest`という指定も可能です。

### モデル保存時の追加情報
`save_model`メソッドでのモデル保存時に、追加の情報やオブジェクトを付与することもできます。
- ラベル `labels`: モデルを管理しやすくするためにユーザー定義したラベル。例: 開発チーム名、ステージ名(dev,prd)
- メタデータ `metadata`: モデル関連情報を付加するユーザー定義したメタデータ。例: 精度、データセットバージョン
- カスタムオブジェクト `custom_objects`: モデルと一緒に保存されるユーザー定義の追加Pythonオブジェクト、定義ファイル等。

### 保存されたモデルの取り出し
モデルオブジェクトの取り出しは`load_model`メソッドを使います。
```python: モデルオブジェクトの取得
import bentoml
from sklearn.base import BaseEstimator

model: BaseEstimator = bentoml.sklearn.load_model("iris_clf:latest")
```

また、ラベル、メタデータ、カスタムオブジェクトの情報を取り出すには`get`メソッドを使います。
```python: モデル情報の取得
import bentoml
bento_model: bentoml.Model = bentoml.models.get("iris_clf:latest")

print(bento_model.tag)
print(bento_model.custom_objects)
print(bento_model.info.metadata)
print(bento_model.info.labels)
```

### BentoML model storeの管理
BentoML model storeの実体は、デフォルトではローカルPCのホームディレクトリの`~/bentoml/models`ディレクトリになります。保存された学習済モデルは`bentoml models`コマンドを通じて管理します。
```sh:（ターミナルで実行）BentoML model storeの管理コマンド
# 保存されたモデルの一覧確認
bentoml models list
# モデル情報の確認
bentoml models get <モデルタグ名>
# モデルの削除
bentoml models delete <モデルタグ名>
```

## Step2. 推論サービスの作成
外部から推論が実行できるためにBentoMLサービスを作成します。これはmodel runnerとAPIで構成されます。構成イメージを図にすると下記のようになります。

![service.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/3d345b6d-a768-5524-7b8b-1c33125a593b.png)


[チュートリアル](https://docs.bentoml.org/en/latest/tutorial.html)にも記載されていますが、コード化すると下記のようになります。
```python:推論サービス(service.py)
import numpy as np
import bentoml
from bentoml.io import NumpyNdarray

iris_clf_runner = bentoml.sklearn.get("iris_clf:latest").to_runner()

svc = bentoml.Service("iris_classifier", runners=[iris_clf_runner])

@svc.api(input=NumpyNdarray(), output=NumpyNdarray())
def classify(input_series: np.ndarray) -> np.ndarray:
    result = iris_clf_runner.predict.run(input_series)
    return result
```

### model runnerの定義
model runnerは学習済モデルを取り込んで推論ができるようにしたユニットと捉えればよいです。model runnerはBentoML model storeから学習済モデルを`get`メソッドで取り出して、`to_runner()`メソッドを呼び出すことで取得できます。
```python: model runner定義の部分
iris_clf_runner = bentoml.sklearn.get("iris_clf:latest").to_runner()
```

ちなみに、model runnerは推論ができるユニットですので、これだけでも動作テストは可能です。以下のようなロジックで実行できます。
```python:model runnerの動作テスト
iris_clf_runner = bentoml.sklearn.get("iris_clf:latest").to_runner()

# runnerを初期化してこのプロセス内で呼び出せる状態にします
iris_clf_runner.init_local()

# 推論の実行
iris_clf_runner.predict.run([[5.9, 3., 5.1, 1.8]])
```

### APIの定義
APIは外部のクライアントから呼び出すことができるロジックのユニットになります。
```python:API定義の部分
@svc.api(input=NumpyNdarray(), output=NumpyNdarray())  # 入出力の定義
def classify(input_array: np.ndarray) -> np.ndarray:
    # == 必要に応じてビジネスロジックや前処理ロジックを記述します ==
    result = iris_clf_runner.predict.run(input_array)  # 推論の呼び出し
    # == 必要に応じて後処理ロジックを記述します ==
    return result
```
- APIは1つの関数で定義します。その関数にデコレーターを付与します。
- デコレーターではAPIの入出力を定義します。この定義では`IO descriptors`というものを使用します。上記例では入出力ともに`numpy.ndarray`の型を指定しています。その他の IO descriptors については[こちら](https://docs.bentoml.org/en/latest/concepts/service.html#built-in-types)を参照してください。
- 関数の中でmodel runnerを呼び出すことで推論を実行します。また、推論の前後で前処理や後処理などのロジック追加も可能です。

### 推論サービスの起動
推論サービスのpythonプログラムを作成したら、実際にサービスとして起動することができます。
```sh:（ターミナルで実行）推論サービスの起動
bentoml serve service:svc
```
- デフォルトではlocalhost:3000でサーバが起動します。
- デフォルトでは、エンドポイント＝API関数名になります。（エンドポイントはデコレーターで独自に指定することも可能です。）
    ```sh:（ターミナルで実行）外部からの推論サービスの呼び出し例
    curl -X POST \
    -H "content-type: application/json" \
    --data "[[5.9, 3, 5.1, 1.8]]" \
    http://127.0.0.1:3000/classify
    ```

## Step3. ビルド
Step2までで推論サービスの実行ができるようになりました。これを別のシステム環境でも動作できるように1つのアーカイブファイルに纏めることを行います。具体的には`Bento`と呼ばれるBentoML独自のアーカイブファイルとして纏めます。この中にはソースコード、学習済モデル、データファイル、設定ファイルなど、サービスを動作せるのに必要なファイルが、まさに"お弁当🍱"のように敷き詰められたものです。

![bento.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/f51e45cc-4ff7-e49c-d7c5-9f260f9860a1.png)

### ビルドファイル
ビルドをするにあたってはビルドの定義ファイル`bentofile.yaml`を作成します。こちらは[チュートリアル](https://docs.bentoml.org/en/latest/tutorial.html)と同様の定義ファイルです。
```yaml:bentofile.yaml
service: "service:svc"
labels:
    owner: bentoml-team
    stage: dev
include:
- "*.py"  # Bentoに含めるファイルのパターン
python:
    packages:  # サービスに必要なpythonパッケージ
    - scikit-learn
    - pandas
```
上記のビルドファイルは見ただけでも分かるごくシンプルなのものですが、実際には様々なオプションが用意されていますので詳細は[公式ドキュメント](https://docs.bentoml.org/en/latest/concepts/bento.html#bento-build-options)を確認してください。


### ビルドの実行
ビルドは`bentoml build`コマンドで実行します。
```sh:（ターミナルで実行）ビルドの実行
bentoml build
```
このコマンドはbentofile.yamlやservice.pyが存在するディレクトリ（=ビルドコンテキスト）で実行します。そこが異なる場合は、そのパスを指定します。また、ビルドファイルを別名で指定する場合は`-f`オプションで指定します。
```sh:（ターミナルで実行）ビルドコンテキスト、ビルドファイルの明示的指定
# 書式: bentoml build [OPTIONS] [BUILD_CTX]
bentoml build -f ./src/my_project_a/bento_fraud_detect.yaml ./src/
```

ビルドが成功すると下記メッセージが表示されます。生成されたBentoには一意のBentoタグが自動的に付けられます。
```
Building BentoML service "iris_classifier:viwdyfu54cv4qaav" from build context "/home/ubuntu/direction3/08_bentoml/quickstart".
Packing model "iris_clf:42c6hfu3r2bsmaav"
Locking PyPI package versions.
WARNING: the legacy dependency resolver is deprecated and will be removed in future versions of pip-tools. The default resolver will be changed to 'backtracking' in pip-tools 7.0.0. Specify --resolver=backtracking to silence this warning.

██████╗░███████╗███╗░░██╗████████╗░█████╗░███╗░░░███╗██╗░░░░░
██╔══██╗██╔════╝████╗░██║╚══██╔══╝██╔══██╗████╗░████║██║░░░░░
██████╦╝█████╗░░██╔██╗██║░░░██║░░░██║░░██║██╔████╔██║██║░░░░░
██╔══██╗██╔══╝░░██║╚████║░░░██║░░░██║░░██║██║╚██╔╝██║██║░░░░░
██████╦╝███████╗██║░╚███║░░░██║░░░╚█████╔╝██║░╚═╝░██║███████╗
╚═════╝░╚══════╝╚═╝░░╚══╝░░░╚═╝░░░░╚════╝░╚═╝░░░░░╚═╝╚══════╝

Successfully built Bento(tag="iris_classifier:viwdyfu54cv4qaav").
```

### Bento storeの管理
生成されたBentoは`Bento store`で管理されます。BentoML storeの実体は、デフォルトではローカルPCのホームディレクトリの`~/bentoml/bentos`ディレクトリになります。保存されたBentoはbentomlのコマンドを通じて管理します。
```sh:（ターミナルで実行）Bento storeの管理コマンド
# 保存されたBentoの一覧確認
bentoml list
# Bento情報の確認
bentoml get <Bentoタグ名>
# Bentoの削除
bentoml delete <Bentoタグ名>
```

## Step4. デプロイ
最後のステップのデプロイです。BentoMLでは、Dockerコンテナとしてデプロイ、`Yatai`のサービスを通じてKubernetes環境にデプロイ、`bentoctl`を利用してクラウド環境にデプロイ、の3パターンがあります。ここではDockerコンテナでのデプロイを記載します。

Dockerの場合は手順としては簡単で、BentoMLのCLIを使ってDockerイメージを生成し、DockerコマンドでDockerコンテナを起動する、流れになります。


![deploy.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/69861f24-1357-af1e-473f-5c665a07f7ec.png)


```sh:Dockerイメージの生成
# Bento storeに格納されているBentoの確認
bentoml list

# Dockerイメージの生成
bentoml containerize iris_classifier:latest
```

Dockerイメージが生成されて`docker image`コマンドで確認すると、REPOSITORYがBentoのサービス名、TAGがBentoの識別子になっていることが確認できます。

- bento listの実行
![bento_list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/5efbacc6-7b1f-ae23-4b43-85ae6a5d0477.png)

- docker imageの実行
![docker image](./docker_image.png)
![docker_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724847/cf9bbfe5-b92a-7bc1-361f-c0a33d09fd15.png)


最後は`docker run`でDockerコンテナを実行します。
```sh:Dockerコンテナ実行
docker run -p 3000:3000 iris_classifier:fm454nu24odx4aav
```

# おわりに
今回はBentoMLの基本的な機能と操作方法を整理しましたが、BentoMLでバッチ推論や非同期推論のサービスも定義できますし、他のMLOps関連のフレームワークとの連携も可能です。更に進んだ利用方法については今後調べていこうと思っています。