ホーム » MONAI » MONAI 0.7 : tutorials : 配備 – BentoML による MedNIST 分類器の配備

MONAI 0.7 : tutorials : 配備 – BentoML による MedNIST 分類器の配備

MONAI 0.7 : tutorials : 配備 – BentoML による MedNIST 分類器の配備 (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 10/19/2021 (0.7.0)

* 本ページは、MONAI の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

 

MONAI 0.7 : tutorials : 配備 – BentoML による MedNIST 分類器の配備

これは MONAI ネットワークを訓練して BentoML を web サーバとして使用して配備するサンプルです、BentoML レポジトリをローカルで使用するかコンテナサービスとして使用します。

このノートブックは BentoML を使用して訓練済みモデルをアーティファクトにパッケージ化するプロセスを実演します、これは推論を実行するローカルプログラムとして、同じことを行なう web サービスとして、そして Docker コンテナ化された web サービスとして実行できます。BentoML は AWS や Azure のような既存のプラットフォームでモデルを配備する様々な方法を提供しますが、ここではローカル配備にフォーカスします、研究者はこれを行なう傾向にあるためです。このチュートリアルは ここの MONAI チュートリアル のような MedNIST 分類器を訓練してから BentoML チュートリアル で説明されているパッケージ化を行ないます。

 

環境のセットアップ

!python -c "import monai" || pip install -q "monai-weekly[pillow, tqdm]"
!python -c "import bentoml" || pip install -q bentoml

 

インポートのセットアップ

# Copyright 2020 MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import shutil
import tempfile
import glob
import PIL.Image
import torch
import numpy as np

from ignite.engine import Events

from monai.apps import download_and_extract
from monai.config import print_config
from monai.networks.nets import DenseNet121
from monai.engines import SupervisedTrainer
from monai.transforms import (
    AddChannel,
    Compose,
    LoadImage,
    RandFlip,
    RandRotate,
    RandZoom,
    ScaleIntensity,
    EnsureType,
)
from monai.utils import set_determinism

set_determinism(seed=0)

print_config()
MONAI version: 0.4.0+119.g9898a89
Numpy version: 1.19.2
Pytorch version: 1.7.1
MONAI flags: HAS_EXT = False, USE_COMPILED = False
MONAI rev id: 9898a89d24364a9be3525d066a7492adf00b9e6b

Optional dependencies:
Pytorch Ignite version: 0.4.2
Nibabel version: 3.2.1
scikit-image version: 0.18.1
Pillow version: 8.1.0
Tensorboard version: 2.4.1
gdown version: 3.12.2
TorchVision version: 0.8.2
ITK version: 5.1.2
tqdm version: 4.56.0
lmdb version: 1.0.0
psutil version: 5.8.0

For details about installing the optional dependencies, please visit:
    https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies

 

データセットをダウンロードする

MedMNIST データセットは TCIA, RSNA Bone Age チャレンジNIH Chest X-ray データセット からの様々なセットから集められました。

データセットは Dr. Bradley J. Erickson M.D., Ph.D. (Department of Radiology, Mayo Clinic) のお陰により Creative Commons CC BY-SA 4.0 ライセンス のもとで利用可能になっています。

MedNIST データセットを使用する場合、出典を明示してください。

directory = os.environ.get("MONAI_DATA_DIRECTORY")
root_dir = tempfile.mkdtemp() if directory is None else directory
print(root_dir)

resource = "https://drive.google.com/uc?id=1QsnnkvZyJPcbRoV_ArW8SnE1OTuoVbKE"
md5 = "0bc7306e7427e00ad1c5526a6677552d"

compressed_file = os.path.join(root_dir, "MedNIST.tar.gz")
data_dir = os.path.join(root_dir, "MedNIST")
if not os.path.exists(data_dir):
    download_and_extract(resource, compressed_file, root_dir, md5)
MedNIST.tar.gz: 0.00B [00:00, ?B/s]
/tmp/tmpxxp5z205
MedNIST.tar.gz: 59.0MB [00:04, 15.4MB/s]                              
downloaded file: /tmp/tmpxxp5z205/MedNIST.tar.gz.
Verified 'MedNIST.tar.gz', md5: 0bc7306e7427e00ad1c5526a6677552d.
Verified 'MedNIST.tar.gz', md5: 0bc7306e7427e00ad1c5526a6677552d.
subdirs = sorted(glob.glob(f"{data_dir}/*/"))

class_names = [os.path.basename(sd[:-1]) for sd in subdirs]
image_files = [glob.glob(f"{sb}/*") for sb in subdirs]

image_files_list = sum(image_files, [])
image_class = sum(([i] * len(f) for i, f in enumerate(image_files)), [])
image_width, image_height = PIL.Image.open(image_files_list[0]).size

print(f"Label names: {class_names}")
print(f"Label counts: {list(map(len, image_files))}")
print(f"Total image count: {len(image_class)}")
print(f"Image dimensions: {image_width} x {image_height}")
Label names: ['AbdomenCT', 'BreastMRI', 'CXR', 'ChestCT', 'Hand', 'HeadCT']
Label counts: [10000, 8954, 10000, 10000, 10000, 10000]
Total image count: 58954
Image dimensions: 64 x 64

 

セットアップと訓練

ここでは変換シークエンスを作成してネットワークを訓練します、検証とテストはこれが実際に動作することを私達は知っていてそしてここでは必要ないので省略します :

train_transforms = Compose(
    [
        LoadImage(image_only=True),
        AddChannel(),
        ScaleIntensity(),
        RandRotate(range_x=np.pi / 12, prob=0.5, keep_size=True),
        RandFlip(spatial_axis=0, prob=0.5),
        RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5),
        EnsureType(),
    ]
)
class MedNISTDataset(torch.utils.data.Dataset):
    def __init__(self, image_files, labels, transforms):
        self.image_files = image_files
        self.labels = labels
        self.transforms = transforms

    def __len__(self):
        return len(self.image_files)

    def __getitem__(self, index):
        return self.transforms(self.image_files[index]), self.labels[index]


# just one dataset and loader, we won't bother with validation or testing 
train_ds = MedNISTDataset(image_files_list, image_class, train_transforms)
train_loader = torch.utils.data.DataLoader(train_ds, batch_size=300, shuffle=True, num_workers=10)
device = torch.device("cuda:0")
net = DenseNet121(spatial_dims=2, in_channels=1, out_channels=len(class_names)).to(device)
loss_function = torch.nn.CrossEntropyLoss()
opt = torch.optim.Adam(net.parameters(), 1e-5)
max_epochs = 5
def _prepare_batch(batch, device, non_blocking):
    return tuple(b.to(device) for b in batch)


trainer = SupervisedTrainer(device, max_epochs, train_loader, net, opt, loss_function, prepare_batch=_prepare_batch)


@trainer.on(Events.EPOCH_COMPLETED)
def _print_loss(engine):
    print(f"Epoch {engine.state.epoch}/{engine.state.max_epochs} Loss: {engine.state.output[0]['loss']}")


trainer.run()
Epoch 1/5 Loss: 0.231450617313385
Epoch 2/5 Loss: 0.07256477326154709
Epoch 3/5 Loss: 0.04309789836406708
Epoch 4/5 Loss: 0.04549304023385048
Epoch 5/5 Loss: 0.025731785222887993

ここでネットワークが Torchscript オブジェクトとしてセーブされますが後で見るようこれは必要ありません。

torch.jit.script(net).save("classifier.zip")

 

BentoML セットアップ

BentoML はサービスリクエストをメソッド呼び出しとしてラップする API を通してプラットフォームを提供します。これは明らかに Flask が動作する方法と似ていますが (これはここで使用される基礎技術の一つです)、これの上にはネットワーク (アーティファクト) のストア、リクエストの IO コンポーネントの処理、そしてデータのキャッシュのための様々な機能が提供されます。私達が提供する必要があるものは望むサービスを表わすスクリプトファイルで、BentoML は提供するアーティファクトと一緒にこれを取得して別の場所にストアします、これはローカルで実行したりサーバにアップロードすることができます (Docker レジストリのようなものです)。

下のスクリプトは MONAI コードを含む API を作成します。変換シークエンスはデータストリームを画像に変えるために特殊な読み取り変換 (= read Transform) を必要としますが、それ以外は訓練のために上で使用されたようなコードです。ネットワークはアーティファクトとしてストアされ、これは実際には BentoML バンドルでストアされた重みです。これは実行時に自動的にロードされますが、望むならば代わりに Torchscript モデルをロードすることもできるでしょう、特に MONAI コードに依存しない API を望む場合には。

スクリプトは最初にファイルに書き出される必要があります :

%%writefile mednist_classifier_bentoml.py

from typing import BinaryIO, List
import numpy as np
from PIL import Image
import torch

from monai.transforms import (
    AddChannel,
    Compose,
    Transform,
    ScaleIntensity,
    EnsureType,
)

import bentoml
from bentoml.frameworks.pytorch import PytorchModelArtifact
from bentoml.adapters import FileInput, JsonOutput
from bentoml.utils import cached_property

MEDNIST_CLASSES = ["AbdomenCT", "BreastMRI", "CXR", "ChestCT", "Hand", "HeadCT"]


class LoadStreamPIL(Transform):
    """Load an image file from a data stream using PIL."""

    def __init__(self, mode=None):
        self.mode = mode

    def __call__(self, stream):
        img = Image.open(stream)

        if self.mode is not None:
            img = img.convert(mode=self.mode)

        return np.array(img)


@bentoml.env(pip_packages=["torch", "numpy", "monai", "pillow"])
@bentoml.artifacts([PytorchModelArtifact("classifier")])
class MedNISTClassifier(bentoml.BentoService):
    @cached_property
    def transform(self):
        return Compose([LoadStreamPIL("L"), AddChannel(), ScaleIntensity(), EnsureType()])

    @bentoml.api(input=FileInput(), output=JsonOutput(), batch=True)
    def predict(self, file_streams: List[BinaryIO]) -> List[str]:
        img_tensors = list(map(self.transform, file_streams))
        batch = torch.stack(img_tensors).float()

        with torch.no_grad():
            outputs = self.artifacts.classifier(batch)
        _, output_classes = outputs.max(dim=1)

        return [MEDNIST_CLASSES[oc] for oc in output_classes]
Overwriting mednist_classifier_bentoml.py

今はスクリプトがロードされて分類器アーティファクトはネットワーク状態とともにパックされます。そしてこれはローカルマシンのレポジトリ・ディレクトリに保存されます :

from mednist_classifier_bentoml import MedNISTClassifier  # noqa: E402

bento_svc = MedNISTClassifier()
bento_svc.pack('classifier', net.cpu().eval())

saved_path = bento_svc.save()

print(saved_path)
[2021-03-02 00:39:04,202] WARNING - BentoML by default does not include spacy and torchvision package when using PytorchModelArtifact. To make sure BentoML bundle those packages if they are required for your model, either import those packages in BentoService definition file or manually add them via `@env(pip_packages=['torchvision'])` when defining a BentoService
[2021-03-02 00:39:04,204] WARNING - pip package requirement torch already exist
[2021-03-02 00:39:05,494] INFO - BentoService bundle 'MedNISTClassifier:20210302003904_AC4A5D' saved to: /home/localek10/bentoml/repository/MedNISTClassifier/20210302003904_AC4A5D
/home/localek10/bentoml/repository/MedNISTClassifier/20210302003904_AC4A5D

このレポジトリの内容を見ることができます、これはコードとセットアップ・スクリプトを含みます :

!ls -l {saved_path}
total 44
-rwxr--r-- 1 localek10 bioeng 2411 Mar  2 00:39 bentoml-init.sh
-rw-r--r-- 1 localek10 bioeng  875 Mar  2 00:39 bentoml.yml
-rwxr--r-- 1 localek10 bioeng  699 Mar  2 00:39 docker-entrypoint.sh
-rw-r--r-- 1 localek10 bioeng 1205 Mar  2 00:39 Dockerfile
-rw-r--r-- 1 localek10 bioeng   70 Mar  2 00:39 environment.yml
-rw-r--r-- 1 localek10 bioeng   72 Mar  2 00:39 MANIFEST.in
drwxr-xr-x 4 localek10 bioeng 4096 Mar  2 00:39 MedNISTClassifier
-rw-r--r-- 1 localek10 bioeng    5 Mar  2 00:39 python_version
-rw-r--r-- 1 localek10 bioeng  298 Mar  2 00:39 README.md
-rw-r--r-- 1 localek10 bioeng   69 Mar  2 00:39 requirements.txt
-rw-r--r-- 1 localek10 bioeng 1691 Mar  2 00:39 setup.py

このレポジトリはストアされたプログラムのように実行できます、そこでは使用したい名前と API 名 (“predict”) によりそれを起動してファイルとして入力を提供します :

!bentoml run MedNISTClassifier:latest predict --input-file {image_files[0][0]}
[2021-03-02 00:39:16,999] INFO - Getting latest version MedNISTClassifier:20210302003904_AC4A5D
[2021-03-02 00:39:19,508] WARNING - BentoML by default does not include spacy and torchvision package when using PytorchModelArtifact. To make sure BentoML bundle those packages if they are required for your model, either import those packages in BentoService definition file or manually add them via `@env(pip_packages=['torchvision'])` when defining a BentoService
[2021-03-02 00:39:19,508] WARNING - pip package requirement torch already exist
[2021-03-02 00:39:20,329] INFO - {'service_name': 'MedNISTClassifier', 'service_version': '20210302003904_AC4A5D', 'api': 'predict', 'task': {'data': {'uri': 'file:///tmp/tmphl16qkwk/MedNIST/AbdomenCT/006160.jpeg', 'name': '006160.jpeg'}, 'task_id': '6d4680de-f719-4e04-abde-00c7d8a6110d', 'cli_args': ('--input-file', '/tmp/tmphl16qkwk/MedNIST/AbdomenCT/006160.jpeg'), 'inference_job_args': {}}, 'result': {'data': '"Hand"', 'http_status': 200, 'http_headers': (('Content-Type', 'application/json'),)}, 'request_id': '6d4680de-f719-4e04-abde-00c7d8a6110d'}
"Hand"

サービスはまた Flask web サーバでも実行できます。以下のスクリプトはサービスを開始し、進むのを待ち、予測を得るために POST リクエストとしてテストファイルを送るために curl を使用して、そしてサーバを kill します :

%%bash -s {image_files[0][0]}
# filename passed in as an argument to the cell
test_file=$1

# start the Flask-based server, sending output to /dev/null for neatness
bentoml serve --port=8000 MedNISTClassifier:latest &> /dev/null &

# recall the PID of the server and wait for it to start
lastpid=$!
sleep 5

# send the test file using curl and capture the returned string
result=$(curl -s -X POST "http://127.0.0.1:8000/predict" -F image=@$test_file)
# kill the server
kill $lastpid

echo "Prediction: $result"
Prediction: "AbdomenCT"

The service can be packaged as a Docker container to be started elsewhere as a server:

!bentoml containerize MedNISTClassifier:latest -t mednist-classifier:latest
[2021-03-02 00:40:48,846] INFO - Getting latest version MedNISTClassifier:20210302003904_AC4A5D
Found Bento: /home/localek10/bentoml/repository/MedNISTClassifier/20210302003904_AC4A5D
Containerizing MedNISTClassifier:20210302003904_AC4A5D with local YataiService and docker daemon from local environment\WARNING: No swap limit support
|Build container image: mednist-classifier:latest
!docker image ls
REPOSITORY               TAG                             IMAGE ID       CREATED          SIZE
mednist-classifier       latest                          326ab3f07478   15 seconds ago   2.94GB
<none>                   <none>                          87e9c5c97297   2 days ago       2.94GB
<none>                   <none>                          cb62f45a9163   2 days ago       1.14GB
bentoml/model-server     0.11.0-py38                     387830631375   6 weeks ago      1.14GB
sshtest                  latest                          1be604ad1135   3 months ago     225MB
ubuntu                   20.04                           9140108b62dc   5 months ago     72.9MB
ubuntu                   latest                          9140108b62dc   5 months ago     72.9MB
nvcr.io/nvidia/pytorch   20.09-py3                       86042df4bd3c   5 months ago     11.1GB
pytorch/pytorch          1.6.0-cuda10.1-cudnn7-runtime   6a2d656bcf94   7 months ago     3.47GB
pytorch/pytorch          latest                          6a2d656bcf94   7 months ago     3.47GB
python                   3.7                             22c70bba8283   7 months ago     920MB
ubuntu                   16.04                           c522ac0d6194   7 months ago     126MB
python                   3.7-alpine                      6a5ca85ed89b   9 months ago     72.5MB
alpine                   3.12                            a24bb4013296   9 months ago     5.57MB
hello-world              latest                          bf756fb1ae65   14 months ago    13.3kB
if directory is None:
    shutil.rmtree(root_dir)
 

以上



ClassCat® Chatbot

人工知能開発支援

◆ クラスキャットは 人工知能研究開発支援 サービスを提供しています :
  • テクニカルコンサルティングサービス
  • 実証実験 (プロトタイプ構築)
  • アプリケーションへの実装
  • 人工知能研修サービス
◆ お問合せ先 ◆
クラスキャット
セールス・インフォメーション
E-Mail:sales-info@classcat.com

カテゴリー