LoginSignup
12
9

More than 3 years have passed since last update.

TorchServeとDocker使ってPyTorchモデルをAPI化してみた

Last updated at Posted at 2021-02-17

はじめに

過去の記事「TorchServeを使ってAzure 環境でPyTorchモデルをAPI化してみた」の続きみたいなものです。

前回は、TorchServeで「できることの紹介」というイメージで基本的な利用方法を浅く広く紹介しました。
今回は、「実際に活用する」という観点に立ち各種PythonファイルやDockerFileを実際に一つ一つ作成します。
そして前回とは異なり、TorchServeをDockerコンテナとして動かしています。
(前回は、TorchServeをプロセスとして直接動作させていました。)

参考にした記事

今回の記事を書くにあたっては以下のページをかなり参考にしました。

上記ページと比較した本記事の特徴は、「とりあえず動かせる」だけでなく「使い方を理解して動かせる」まで目指している点だと思います。

ゴールイメージ

このような形で、画像を送ると結果が返ってくるようなAPIをDockerコンテナ上に作成することを目指します。
今回は、APIとしてホストされるモデルはvgg11を用います。(vgg11である理由は特にないです)
image.png

前提

今回は、前回の記事で作成したAzure VMでにSSHで接続している状態を前提として手順の紹介を行っています。
もしVMが未作成の方は、以下の作業を先にお願いします。

Azure VMの作成

以下の設定で、Azure VMを作成します。

設定項目 設定値
イメージ Ubuntu Server18.04LTS- Gen1
リージョン 米国中南米
サイズ Standard_NC6_Promo - 6 vcpu
その他 デフォルト

cuda関連パッケージのインストール

具体的な手順は、以前の記事をご参照ください。

手順

手順は以下のような流れとなっています。

Dockerのインストール

まずは、以下のコマンドを実行し、Azure VMにDockerをインストールします。

dockerをインストール
sudo apt-get update

sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io

フォルダ構成の用意

Dockerのインストールが終了したら、今回の作業用ディレクトリtorchserve-dockerを作成します。
作業用ディレクトリは今回は、ホームディレクトリ直下に作成します。
その後、作成した作業用ディレクトリに移動します。

作業用ディレクトリ作成
cd ~
mkdir torchserve-docker
cd torchserve-docker

そして、作業用ディレクトリの配下に以下のようなフォルダ構成を用意します。

ディレクトリ構造
torchserve-docker
├── .docker_torchserve_deploy // Dockerファイルを作成するためのディレクトリ
├── model_store  // モデルアーカイブファイルを保存するディレクトリ
└── models // モデルのクラスファイル、学習済みの重みファイルなどを保存するディレクトリ
    └── vgg11

SSL化用の証明書作成

今回は、SSLで通信できるようにAPIを構成します。
そこで、以下のコマンドを実行し、APIをSSL化するための秘密鍵と証明書(兼公開鍵)を生成します。
ここでは、opensslを使って自己署名証明書を作成しています。

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout mykey.key -out mycert.pem

設定値は以下のようにしました。

設定の例
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Shinagawa
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Org
Organizational Unit Name (eg, section) []:OrgUnit
Common Name (e.g. server FQDN or YOUR name) []:torchserve-sample
Email Address []:example@example.com

これで、SSL用の秘密鍵と証明書が作成されました。lsコマンドで確認すると、以下のようになります。

出力結果
model_store  models  mycert.pem  mykey.ke

TorchServeのサーバー用設定ファイルを用意

次に、サーバーに関する設定を記述するための設定ファイルを用意します。
以下のコマンドで、カレント・ディレクトリにconfig.propertiesというファイルを作成してください。

config.properties作成
touch config.properties

なお、config.propertiesで設定できる内容についての詳細は、こちらのTorchServeドキュメントに記載してあります。

続いて、作成したファイルに、外部からアクセスするためのIPアドレスと、SSL通信に利用するためにさきほど作成した秘密鍵と証明書のパスを設定します。
config.propertiesに以下のように設定をしてください。

config.properties
inference_address=https://0.0.0.0:8443
management_address=https://0.0.0.0:8444
metrics_address=https://0.0.0.0:8445
private_key_file=mykey.key
certificate_file=mycert.pem

ここで設定している3つのIPアドレスは、それぞれ以下の3つのAPIに対応しています。

APIの種類 概要 デフォルトのアドレス ドキュメント
推論用API モデルを使って推論をする際に使うエンドポイントです http://127.0.0.1:8080 Inference API
モデル管理用API モデルの登録、ステータス確認、ワーカー数設定といったモデルの管理をする際に使うAPIです。 http://127.0.0.1:8081 Management API
指標用API 指定したモデルの指標を確認するためのエンドポイントです。
また、このAPIを通じてモデルの指標をダッシュボードで閲覧することも可能です。
http://127.0.0.1:8082 Metrics API

また、前回の記事ではそれぞれにAPIについてより詳しく説明しています。

デプロイするモデル(今回はVGG11)の用意

今回は、画像分類用のモデルVGG11をTorchServe上にデプロイします。

そこで、デプロイするために必要な以下の4つのファイルを用意します。

  • PyTorchのVGG11のモデルを記載したPythonファイル(./models/vgg11/model.py)
  • VGG11の学習済みの重みファイル(./models/vgg11/vgg11-bbd30ac9.pth)
  • ハンドラの処理を定義したPythonファイル(./models/vgg11/vgg_handler.py)
  • クラス名とインデックスの対応付けを記載したjsonファイル(./models/index_to_name.json)

PyTorchのVGG11のモデルを記載したPythonファイルを用意

まずは、VGG11のモデルをPyTorchで定義したPythonファイルを用意します。
以下のコマンドで、ファイルを作成してください。

model.pyの作成
touch ./models/vgg11/model.py

このファイルに、以下のようにVGG11のモデルを定義します。

./models/vgg11/model.py
from torchvision.models.vgg import VGG, make_layers, cfgs

class ImageClassifier(VGG):
    """VGGを継承してPyTorchのモデルクラスを定義"""
    def __init__(self):
        # VGGのPyTorchモデルをインスタンス化しているだけ
        super(ImageClassifier, self).__init__(make_layers(cfgs['A'], False), **{'init_weights': False})

ご覧のように、TorchVisionで用意されているVGGのモデルを継承しているだけです。

VGG11の学習済みの重みファイルを用意

次に、以下のコマンドで、PyTorchが公開している学習済みのVGG11の重みファイルを取得します。
ここでは、PytorchのGitHub上で公開されているものをダウンロードしています。

重みファイルの取得
wget https://download.pytorch.org/models/vgg11-bbd30ac9.pth -P "./models/vgg11"

ハンドラの処理を定義したPythonファイルを用意

そして、ハンドラを定義したファイルを作成します。
ハンドラは、TorchServe上でモデルをインスタンス化し、重みをロードする処理などをするPythonファイルです。
今回はファイルを作成しますが、デフォルトのハンドラを使うことも可能です。

以下のコマンドで、ハンドラのファイルを作成します。

vgg_handler.py作成
touch ./models/vgg11/vgg_handler.py

今回は、ハンドラの役割についてイメージを持つために、以下の処理をするメソッドを記載しました。

  • モデルのインスタンス化(def _load_pickled_model()
  • 推論結果の後処理(def postprocess()
./models/vgg11/vgg_handler.py
import importlib
import os
import torch
import torch.nn.functional as F
from torchvision import transforms

from ts.torch_handler.vision_handler import VisionHandler
from ts.utils.util import list_classes_from_module, map_class_to_label

class VGGImageClassifier(VisionHandler):
    """
    VisionHandlerを継承して、VGG用のハンドラを作成。
    """
    topk = 5
    # These are the standard Imagenet dimensions
    # and statistics
    image_processing = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406],
                             std=[0.229, 0.224, 0.225])
    ])

    def _load_pickled_model(self, model_dir, model_file, model_pt_path):
        """モデルのインスタンス化をするメソッド
         具体的には、クラスのファイルと学習済みの重みを読みとり、PyTorchモデルのオブジェクトを返します。"""
        model_def_path = os.path.join(model_dir, model_file)
        if not os.path.isfile(model_def_path):
            raise RuntimeError("Missing the model.py file")
        # 指定したPyTorchモデルを定義したPythonファイルをimport
        module = importlib.import_module(model_file.split(".")[0])
        # Pythonファイルに、モデルのクラスの定義が複数含まれていないか確認
        model_class_definitions = list_classes_from_module(module)
        # 複数含まれている場合はエラー
        if len(model_class_definitions) != 1:
            raise ValueError("Expected only one class as model definition. {}".format(
                model_class_definitions))
        # 一つだけの場合は、そのクラスを取得
        model_class = model_class_definitions[0]
        # 重みのファイルを読み込み
        state_dict = torch.load(model_pt_path)
        # クラスをインスタンス化
        model = model_class()
        # 重みをモデルにロード
        model.load_state_dict(state_dict)
        return model

    def set_max_result_classes(self, topk):
        """推論時、TOPいくつまでの結果を返すかを設定(setter)"""
        self.topk = topk

    def get_max_result_classes(self):
        """推論時、TOPいくつまでの結果を返すかを取得(getter)"""
        return self.topk

    def postprocess(self, data):
        """推論結果を返す際の後処理をするメソッド"""
        ps = F.softmax(data, dim=1)
        probs, classes = torch.topk(ps, self.topk, dim=1)
        probs = probs.tolist()
        classes = classes.tolist()
        # TorchServeのutilを利用して、推論結果のインデックス(数値)、ラベル(文字列)に変換
        # self.mappingは、基底クラスの「BaseHandler」で定義
        return map_class_to_label(probs, self.mapping, classes)

上記ファイルでは、モデルのインスタンス化と、推論結果の後処理のみハンドラに定義しています。
しかし、継承元のクラスでは、さらにデータの前処理なども実装されています。
カスタムのハンドラを定義することで、推論時にさらに複雑な処理を追加することも可能です。

クラス名とインデックスの対応付けを記載したjsonファイルを用意

インデックス(数値)とクラス名(文字列)の対応付けをするためのファイルを用意します。
これは、さきほど定義したハンドラのスクリプトで、推論結果をインデックス→ラベルに変換する際に利用することになります。
以下のコマンドで、TorchServeのGitHubのサンプルをダウンロードし、./modelsフォルダに格納します。

wget https://raw.githubusercontent.com/pytorch/serve/master/examples/image_classifier/index_to_name.json -P "./models"

TorchServeのGitHubのサンプルから直接ダウンロードし、ファイルを作成てい頂いても大丈夫です。
行数が長いので一部だけサンプルとしてファイルの中身を掲載します。

index_to_name.json
{"0": ["n01440764", "tench"], "1": ["n01443537", "goldfish"], "2": ["n01484850", "great_white_shark"], "3": ["n01491361", "tiger_shark"], "4": ["n01494475", "hammerhead"], "5": ["n01496331", "electric_ray"], "6": ["n01498041", "stingray"], "7": ["n01514668", "cock"], "8": ["n01514859", "hen"], "9": ["n01518878", "ostrich"], "10": ["n01530575", "brambling"], 

実行用スクリプトの用意

TorchServeを起動するためのシェルスクリプトが記述されたrun.shファイルを作成します。
このシェルスクリプトは、Dockerコンテナの起動時に実行されることになります。

run.shを作成
tourch run.sh

run.shでは、具体的には以下の処理を行います。

  • デプロイするVGG11のモデルをアーカイブ(torch-model-archiver
    • アーカイブとは、VGG11のモデルを、TorchServeで扱える形式(.mar拡張子)に変換することです。
    • torch-model-archiverコマンド自体については、以前の記事の#モデルを.mar形式に変換で紹介しています。
  • TorchServeの起動(torchserve --start
    • このコマンドで、TorchServeが起動されVGG11はAPI化されます。
    • またこの時、先ほど用意したconfig.propertiesで行った設定が反映され、APIはSSL化されます。
    • こちらもコマンドの意味については、以前の記事の#TochServeの起動に記載しています。
run.sh
#!/bin/bash
set -e

# パスを変数に設定
MODEL_DIR="${WORK_DIR}/models" 
MODEL_FILE_PATH="${MODEL_DIR}/vgg11/model.py" 
SERIALIZED_FILE_PATH="${MODEL_DIR}/vgg11/vgg11-bbd30ac9.pth"
HANDLER_FILE_PATH="${MODEL_DIR}/vgg11/vgg_handler.py"
INDEX_FILE_PATH="${MODEL_DIR}/index_to_name.json"

# デプロイするVGG11のモデルをアーカイブ
torch-model-archiver \
--model-name vgg11 \
--version 1.0 \
--model-file ${MODEL_FILE_PATH} \
--serialized-file ${SERIALIZED_FILE_PATH} \
--export-path ${MODEL_STORE_DIR} \
--handler ${HANDLER_FILE_PATH} \
--extra-files ${INDEX_FILE_PATH}

# TorchServeの起動
torchserve --start \
--model-store ${MODEL_STORE_DIR} \
--ts-config ${WORK_DIR}/config.properties \
--models vgg11=vgg11.mar

# dockerがexitしないためのコマンド
tail -f /dev/null

DockerFileの作成

最後に、DockerFileを作成します。

DockerFileの作成
touch .docker_torchserve_deploy/DockerFile

Dockerファイルは、以下のように設定します。
今回はDockerのマルチステージビルドを利用し、ビルド用イメージを作成した後、改めてデプロイ用イメージを作成するという流れにしています。

.docker_torchserve_deploy/DockerFile
# syntax = docker/dockerfile:experimental
ARG BASE_IMAGE=ubuntu:18.04

# =============ビルド用のイメージを作成=============
FROM ${BASE_IMAGE} AS build-image

ENV PYTHONUNBUFFERED TRUE
# aptのキャッシュディレクトリ「/var/cache/apt」をマウントした上で必要なパッケージをインストール
# 各パラメータの参考:https://qiita.com/ryuichi1208/items/d13ec434e694d672ab36
RUN --mount=type=cache,id=apt-dev,target=/var/cache/apt \
    apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
    ca-certificates \
    g++ \
    python3-dev \
    python3-distutils \
    python3-venv \
    openjdk-11-jre-headless \
    curl \
    && rm -rf /var/lib/apt/lists/* \
    && cd /tmp \
    && curl -O https://bootstrap.pypa.io/get-pip.py \
    && python3 get-pip.py

# Python仮想環境の作成
RUN python3 -m venv /home/venv

# pathにPython仮想環境を追加
ENV PATH="/home/venv/bin:$PATH"

# 利用するPython、pipのバージョンを設定
# update-alternatives --install <シンボリックリンクのパス> <コマンド名> <実体へのパス> <優先度>
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3 1 \
    && update-alternatives --install /usr/local/bin/pip pip /usr/local/bin/pip3 1

# cudaを使う際には設定する
RUN export USE_CUDA=1

# docker buildコマンドの実行時に指定がなかった場合の、環境変数の初期値を設定
ARG CUDA_VERSION=cu110
ARG TORCH_VER=1.7.1
ARG TORCH_VISION_VER=0.8.2

# PyTorch関連のパッケージをインストール
RUN pip install --no-cache-dir torch==$TORCH_VER+$CUDA_VERSION torchvision==$TORCH_VISION_VER+$CUDA_VERSION -f https://download.pytorch.org/whl/torch_stable.html; 
# torchserve関連のパッケージをインストール
RUN pip install --no-cache-dir captum torchtext torchserve torch-model-archiver


# ===========デプロイ用のイメージを作成=============
FROM ${BASE_IMAGE} AS runtime-image

ENV PYTHONUNBUFFERED TRUE
# 実行フォルダ
ENV WORK_DIR /home/model-server
# モデルアーカイブディレクトリ
ENV MODEL_STORE_DIR ${WORK_DIR}/model_store

# カレント・ディレクトリを変更
WORKDIR ${WORK_DIR}

# 二回目のapt-getでは、ビルド時にマウントした「/var/cache/apt」からキャッスを利用してインストール

RUN --mount=type=cache,target=/var/cache/apt \
    apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
    python3 \
    python3-distutils \
    python3-dev \
    openjdk-11-jre-headless \
    build-essential \
    && rm -rf /var/lib/apt/lists/* \
    && cd /tmp

# TorchServe用のユーザー「model-server」を追加
RUN useradd -m model-server \
    && mkdir -p ${WORK_DIR}/tmp \
    $$ mkdir -p ${MODEL_STORE_DIR}

# ビルド用イメージ内にインストールしたPythonモジュールを利用する
COPY --chown=model-server --from=build-image /home/venv /home/venv

# pathにPython仮想環境を追加
ENV PATH="/home/venv/bin:$PATH"

# 8080:REST推論用API、8081:RESTモデル管理用API、8082:REST指標用API
EXPOSE 8443 8444 8445

# 環境変数TEMPを設定
ENV TEMP=${WORK_DIR}/tmp

# 全てのフォルダ・ファイルを実行用フォルダにコピー
COPY . ${WORK_DIR}

# 実行用ディレクトリ配下全ての所有者をmodel-serverに変更する
RUN chown -R model-server ${WORK_DIR}

# ユーザーをmdoel-serverに変更
USER model-server

# モデルのアーカイブと、TorchServeの起動
ENTRYPOINT ["/bin/bash","./run.sh"]

DockerFileのビルドとRUN

作成したDockerFileをビルドします。
が、その前にDockerファイルをビルドする際に利用する変数を設定します。
※具体的な設定値は利用している環境等にあわせて適宜変更してください。

変数の設定
DOCKER_TAG="torchserve-docker:gpu" # Dockerイメージの名前とタグ
BASE_IMAGE="ubuntu:18.04" # ベースイメージを指定
DOCKER_FILE=".docker_torchserve_deploy/Dockerfile" # Dockerファイルまでのパス
CUDA_VERSION="cu110" # CUDAのバージョン
TORCH_VER="1.7.1" # PyTochのバージョン
TORCH_VISION_VER="0.8.2" # TorchVisionのバージョン

続いて以下のコマンドを実行して、Dockerイメージをビルドします。

dockerビルドの実行
sudo DOCKER_BUILDKIT=1 docker build --file $DOCKER_FILE \
 --build-arg BASE_IMAGE=${BASE_IMAGE} \
 --build-arg CUDA_VERSION=${CUDA_VERSION} \
 --build-arg TORCH_VER=${TORCH_VER} \
 --build-arg TORCH_VISION_VER=${TORCH_VISION_VER} \
 -t $DOCKER_TAG .

上記コマンドでは、docker buildの際にDOCKER_BUILDKIT=1を設定することで、Buildkitを有効にしています。
これによって、dockerをビルドした時のキャッシュが保存され、次回から効率的にdockerのビルドが可能になります。

buildkitについては、以下のページがとても参考になりました。

ビルドが問題なく終了したら、以下のコマンドでDocker を実行します。

dockerをrunする
sudo docker run --rm -itd -p 8443:8443 -p 8444:8444 -p 8445:8445  ${DOCKER_TAG}

推論APIの実行

では、デプロイしたAPIに推論リクエストを送ってみましょう。
以下のコマンドで推論をするための画像を取得します。

画像取得
wget https://raw.githubusercontent.com/pytorch/serve/master/docs/images/kitten_small.jpg

画像は、前回の記事と同様の子猫の写真です。
image.png

そして、この画像を推論用APIに対して送信します。
※ 今回は自己署名証明を使っているため、 -kを追加してSSLエラーを無視しています。

推論実行
sudo curl "https://localhost:8443/predictions/vgg11" -T kitten_small.jpg -k

以下のような結果が返ってくれば、推論は成功です。

推論結果
{
   "tabby": 0.3414705693721771,
   "Egyptian_cat": 0.329368531703949,
   "lynx": 0.192706897854805,
   "tiger_cat": 0.0975274071097374,
   "Persian_cat": 0.009637265466153622
}

おわりに

今回は、TorchServeをDockerコンテナー化して利用する方法を紹介しました。
次回は、今回作成したDockerコンテナーをAzure Kubernetes Servicesにデプロイし、認証機能付きのAPIを作成したいと考えています。
(あくまで考えているだけなので悪しからず。。。)

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9