機械学習プロジェクトをいい感じにプロダクトに載せていく今風のやり方について考える

公開日:2018-12-18
最終更新:2018-12-18

機械学習プロジェクトをいい感じにプロダクトに載せていく今風のやり方について考える

この記事は裏freee developers Advent Calendar 2018の18日目の記事です。

どうも、@aflcです。freeeで機械学習とかやってます。freeeだとRoyで通ってます。
今日は、なんとかしてモデルは作ったもののその後どうしよう、という話をします。

TL;DR

TensorFlow Servingkubelessで、サーバーとか何も考えずにデプロイ出来るようになることを目指します。

本日話す内容

  • 機械学習モデルのデプロイ
  • 前処理・後処理の実装
  • モデルのバージョン

話さない内容

  • 評価・テスト
  • 開発時の環境
  • TensorFlow以外で実装する場合
  • APIの設計
  • などなど
  • Python以外の言語の話題

はじめに

あなたが様々な苦労を乗り越え、イカした機械学習モデルを構築できたとします。
しかしながら、あなたがそのモデルを作ったのは、実際にそのモデルを使って実現したい事があったからで、モデルを作ることが目的ではないはずです(一部のアカデミックな人は除く)。

この時問題となるのは、どうやって作ったモデルをアプリケーションの一部として組み込んでいくのか、です。おそらく、やりたい事の規模や複雑さに応じていくつかのパターンがあるかと思います。

今回はWebAPIとして機能を提供する場合について、どうやって効率よく開発できるかについて考えていきます。その他の場合、例えばIoTエッジに組み込んだり、モバイルネイティブアプリに同梱したり、PWAやフロントエンドで実行したい、といったケースは考えません。また、TensorFlowでモデルを作った場合を想定しています

さて、モデルの組み込み方ですが、例えば以下のようなパターンが考えられます。

  1. アプリケーションから直接叩く
  2. モデルをWeb application framework(Flaskなど)で包んでAPIサーバーを作る
  3. モデルをWeb application framework(Flaskなど)で包んでコンテナイメージ化し、AWS Fargate、Azure Container Instance, (Managed) Kubernetesなどで管理する
  4. 学習器の部分と前後の処理をそれぞれマイクロサービス化する
  5. 学習器部分は専用のサーバー実装を利用し、前後処理をサーバーレスフレームワークで実行する

1のパターンはサービス層は薄く、機械学習の結果がメインなプロダクトには向いているかもしれません。この場合、負荷はほぼ機械学習エンジンの実行で、その単位でスケールします。まぁあまり当てはまる例はなさそうではあります。

2, 3のパターンは、初期のフェイズではシンプルで扱いやすいです。APIの設計も柔軟にでき、FlaskやBottleなどでシンプルにラップしたJson APIなりを作ってコンテナ化してしまえば、ポータブルで管理しやすいと思います。

4のパターンは、カスタムな画像処理をしたり、他の学習器の結果を入力に使うなど、wrapper部(=前後処理)が重い場合には有効だと思われます。WrapperとModelを異なるスケールポリシーで管理できるので、より最適な負荷コントロールが出来るはずです。
特に最近だとModel部はGPUや専用のチップを使ってより高効率に動かす機運が高まってる気がするので、分けておくとそのあたりを柔軟に切り替えられると思います。

今回試すのが5のパターンで、学習モデルやコードを自分でデプロイする事を放棄し、それぞれの特性に合わせたデプロイモデルとして、学習器の推論はTensorFlow Servingで、前後処理は簡単にデプロイ出来るサーバーレスの仕組みを使っていきます。

また、今風なデプロイをするために、Kubernetesクラスタ上で動かします。

Kubernetes環境の準備

今回はDocker Desktop for Mac
を使ってDockerとKubernetesのローカル環境を構築しました。
メニューバーを右クリックしてPreference > KubernetesからEnable Kubernetesにチェックを入れてApplyすると全自動でKubernetesがインストールされます。

TensorFlow Servingの準備

今回は手動でspecを書きました。tensorflowというnamespaceにインストールしていきます。

apiVersion: v1  
kind: Namespace  
metadata:  
  name: tensorflow  
---  
apiVersion: apps/v1  
kind: Deployment  
metadata:  
  namespace: tensorflow  
  name: tensorflow-serving  
  labels:  
    app: tensorflow-serving  
spec:  
  replicas: 1  
  strategy:  
    type: RollingUpdate  
    rollingUpdate:  
      maxSurge: 1  
      maxUnavailable: 0  
  selector:  
    matchLabels:  
      app: tensorflow-serving  
  template:  
    metadata:  
      name: tensorflow-serving  
      labels:  
        app: tensorflow-serving  
    spec:  
      containers:  
        - name: tensorflow-serving  
          image: tensorflow/serving  
          ports:  
          - containerPort: 8500  
          - containerPort: 8501  
          args: ["--model_config_file", "/models_config/models.config", "--enable_batching"]  
          volumeMounts:  
            - name: models  
              mountPath: /models  
      volumes:  
        - name: models  
          hostPath:  
            path: /tmp/tensorflow-serving-models  
---  
apiVersion: v1  
kind: Service  
metadata:  
  namespace: tensorflow  
  name: tensorflow-serving  
  labels:  
    app: tensorflow-serving  
spec:  
  type: NodePort  
  selector:  
    app: tensorflow-serving  
  ports:  
    - name: grpc  
      protocol: TCP  
      port: 8500  
      targetPort: 8500  
      nodePort: 32500  
    - name: rest  
      protocol: TCP  
      port: 8501  
      targetPort: 8501  
      nodePort: 32501  

上記設定を適当に保存して(serving.yaml)、kubectl apply -f serving.yamlすればインストール完了です。

今回、テストのために/tmp/tensorflow-serving-modelsの下にモデルを置くようにしてあります。

kubectl get all -n tensorflowしてみて、pod/tensorflow-serving-xxxがREADYになっていれば良いでしょう。

# こんな感じ  
...  
pod/tensorflow-serving-6db95958f-rmmf7   1/1       Running   0          5h  
...  

mnistのモデルをデプロイしてみる

面倒シンプルな方がわかりやすいので、公式の用意したコードを使ってTensorflowのモデルを作ります。

# tensorflowとかはインストールしておいてね  
git clone https://github.com/tensorflow/serving.git  
cd serving  
python tensorflow_serving/example/mnist_saved_model.py mnist_model  

正常に終了すると、mnist_model/1というディレクトリができます。1というのは単におまけで付いてきたのでそんなに意味はありません(そこらへんは手動で管理する)。この中のsaved_model.pbvariablesがTensorFlowのSavedModel形式で、これを用意する事でTensorFlow Servingで読み込むことが出来るようになります。よく見るCheckpoint形式じゃないので注意。

ちなみに、サンプルのスクリプトよりもう少し簡単に作るメソッドも用意されています:

tf.saved_model.simple_save(session, export_dir, inputs={"x": x, "y": y}, outputs={"z": z})  
# inputs, outputsはそれぞれTensorを指定する  

このmnist_model/1を、/tmp/tensorflow-serving-models/におもむろにぶちこむと、勝手に変更を検知してモデルをロードしてくれます:

# modelはデフォルトのモデル検索パス  
cp -a mnist_model /tmp/tensorflow-serving-models/model  

TensorFlow Servingを叩いてみる

今回はサンプルをちょっと単純化して、次のようなスクリプトを用意してみました。

# mnist_client.py  
import os  
import time  

import grpc  
import numpy as np  
import tensorflow as tf  
from PIL import Image  

# これをやらないと初回がめっちゃ遅い https://github.com/tensorflow/tensorflow/issues/12043  
from tensorflow.contrib.util import make_tensor_proto  

from tensorflow_serving.apis import predict_pb2, prediction_service_pb2_grpc  


def predict_number(path, is_parallel):  
    img = np.asarray(Image.open(path), dtype="float32").reshape((1, -1))  
    # 画像の読み込み時間は飛ばす  
    host = os.environ.get("TF_SERVING_HOST")  
    channel = grpc.insecure_channel(host)  
    stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)  
    request = predict_pb2.PredictRequest()  
    request.model_spec.name = "model"  
    request.model_spec.signature_name = "predict_images"  
    request.inputs["images"].CopyFrom(tf.contrib.util.make_tensor_proto(img))  
    start_time = time.time()  
    if is_parallel:  
        futures = []  
        for _ in range(1000):  
            futures.append(stub.Predict.future(request))  
        for future in futures:  
            future.result()  
    else:  
        for _ in range(1000):  
            stub.Predict(request)  
    end_time = time.time()  
    print(f"{end_time - start_time:.2}s")  


def _parse_args():  
    import argparse  

    psr = argparse.ArgumentParser()  
    psr.add_argument("path")  
    psr.add_argument("--parallel", default=False, action="store_true")  
    return psr.parse_args()  


if __name__ == "__main__":  
    args = _parse_args()  
    predict_number(args.path, args.parallel)  

以下のようにして画像を与えると、結果が帰ってくるのがわかります(画像はここからもダウンロードできます):

TF_SERVING_HOST=localhost:32500 python mnist_client.py test.png  

尚、7回叩いた後の3回の時間は以下の様になりました。

  • parallel: 0.52s 0.55s 0.37s
  • single: 1.8s 1.5s 1.4s

いい感じにスレッドで?さばいてくれているようです。

ちなみに、TensorFlow Servingのオプションで一定間隔でリクエストをバッファして、まとめてバッチ実行することで効率よく実行する--enable_batchingをON/OFFしてみたのですが、時間は特に変わりませんでした。

kubelessの導入

それでは、ちょっとした前処理として、画像ファイルをPOSTで受け取って予測をする関数を用意してみます。今回はサンプルなので適当ですが、例えば形態素解析をしたり、カスタムな特徴量抽出を行ったりするステージに相当すると考えてください。

kubelessのインストールは公式通りに行います。各自ここを参考にして入れれば素直に入ると思います。

kubelessコマンドが使えるようになったら、以下のスクリプトを用意してmnist_kubeless.pyというファイルとして保存しておきます。
先程のmnist_client.py
を少し改変したものを用意します。

import json  
import os  
from traceback import print_exc  

import grpc  
import numpy as np  
import tensorflow as tf  
from bottle import HTTPResponse  
from PIL import Image  
# これをやらないと初回がめっちゃ遅い https://github.com/tensorflow/tensorflow/issues/12043  
from tensorflow.contrib.util import make_tensor_proto  

from tensorflow_serving.apis import predict_pb2, prediction_service_pb2_grpc  


def predict(event, _):  
    try:  
        return _predict(event)  
    except Exception:  
        print_exc()  
        raise  


def _predict(event):  
    request = event["extensions"]["request"]  
    image_file = request.files.get("image")  
    img = np.asarray(Image.open(image_file.file), dtype="float32").reshape((1, -1))  
    # 画像の読み込み時間は飛ばす  
    host = os.environ.get("TF_SERVING_HOST")  
    channel = grpc.insecure_channel(host)  
    stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)  
    request = predict_pb2.PredictRequest()  
    request.model_spec.name = "model"  
    request.model_spec.signature_name = "predict_images"  
    request.inputs["images"].CopyFrom(tf.contrib.util.make_tensor_proto(img))  
    result = stub.Predict(request)  
    # scores = MessageToDict(result)  
    scores = [x for x in result.outputs["scores"].float_val]  
    # 加工しないならMessageToJsonしても良い  
    resp = HTTPResponse(status=200, body=json.dumps({'result': scores}))  
    resp.set_header("Content-Type", "application/json")  
    print(resp)  
    return resp  

実は、公式のpython runtimeはBottleベースになっているので、返り値は文字列かbottle.HTTPResponseクラスのインスタンスを返せば良いです。json APIにするならそのままdictとかで返してもいいと思います。

あとは、kubeless function deploy <name>で関数をデプロイするだけ!

kubeless function deploy mnist \  
    -d requirements.txt \  
    -f mnist_kubeless.py \  
    --handler mnist_kubeless.predict \  
    -e TF_SERVING_HOST="tensorflow-serving.tensorflow.svc.cluster.local:8500" \  
    -n tensorflow \  
    -r python3.6  

ただ、標準だとkubeless functionは自動的に外部に露出してくれないので、kubectl proxyコマンドで一時的にアクセスできるようにします。

kubectl proxy -p 8080 &  
curl -X POST http://localhost:8080/api/v1/namespaces/tensorflow/services/mnist:http-function-port/proxy/ -F "[email protected]"  
{"result": [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]}%  

kubeless function deploy --dryrunすると、applyするつもりだったyamlが見れるので、興味がある人は見てみるのが良いでしょう。

外部にサービスを露出するにはIngress controllerを使うのが推奨されているようですが、今回はここまで。

おわりに

今回はTensorFlow Servingとkubelessで実装を行いましたが、このようにモデルとその周りの実装とを分けておくと、一部を他のプロジェクトで置き換えたり大手のサービスを使ったり出来るので、柔軟性も確保できると考えています。

例えば、モデルのデプロイには

  • GCPのCloud ML Engine: TensorFlow, scikit-learn, XGBoost
  • AWSのSageMaker: TensorFlow, Apache MXNet, PyTorch, Chainer, Scikit-learn, SparkML, Horovod, Keras, Gluon
  • AzureのKubernetes環境(AKS)を使う: 任意のコンテナイメージを使う感じっぽい

を上手く使って楽をしてもいいでしょう。それ以外だといくつかの面白いOSSもあります:

  • mlflow: 機械学習プロジェクトのライフサイクルを管理するフレームワーク。DataBricks社製。SageMakerへのデプロイも出来るらしい。まだベータで機能は整っていない感じ
  • kubeflow: こちらも実験からデプロイまで面倒見るタイプのフレームワーク。Google
  • Seldon: 複数フレームワークに対応したデプロイツール。kubeflowも利用している。モデルのA/Bテストなど、複数モデルをいろいろ制御する仕組みが特徴っぽい。 Seldon Technologies社製。ドキュメントがわかりにくい
  • GraphPipe: Tensorflow, Caffe2, and ONNXに対応したデプロイフレームワーク。超速らしい。良さそうなのに、現在Github Starが552しかない。Oracleだからなのか?

などなど。kubeflow, mlflowはデプロイ用として使うのは違う感じがしますが、UIでモデルのバージョンを管理したり、学習を自動化したり、という目的もあるなら乗ってみてもいいかもしれません。

他にもたくさんあるんですが、まだまだデファクトといった感じのものはない印象なので、大手のクラウドプロバイダのフレームワークにちょっとしたラッパーを書いて運用したほうがまだ楽そうです。

一方、サーバーレスの世界もたくさんの選択肢があって、最近だとKnativeが発表されたり、AWS Lambdaで任意のランタイムが動くようになり、Layersでnumpyなど今まで面倒だったライブラリをまとめて管理できるようになったりしています。

また、今回は紹介しませんでしたが、Serverless frameworkという複数のFaaSサービスを共通のフォーマットで扱うツールなんかもあるので、このへんは自身の周りの環境や要件に合わせて柔軟に決められるのではと思います。

ちなみに今私が検討しているのはkubelessとKnativeとLambdaで、それぞれ以下のような感想を抱いています。

  • kubeless
    • pros
      • 関数単位でデプロイ出来る
      • 依存関係管理が楽(requirements.txtベース)
      • カスタムイメージ・カスタムランタイムも作れる
      • horizontal pod autoscalerを使って簡単にスケール出来る
    • cons
      • vendoringするなど、でかい関数を扱うのは一癖必要
      • pub/sub -> kafka, routing -> ingressなど、シンプルであるが故にいろいろやろうとすると追加で設定する必要がある
  • Knative
    • pros
      • 圧倒的Google力感
      • Zero podに出来るスケーラビリティ
      • 最初から色々入っている
        • カスタムイメージビルド
        • Istio
        • Zipkin
        • など
    • cons
      • 機械学習の前後処理に使うのはオーバースペック気味
      • zero podになった時のタイムラグが気になる(8秒くらいらしい)
      • コンテナイメージをいちいち作るのが面倒(多分関数単位でデプロイできない?)
  • AWS Lambda
    • pros
      • 関数単位でデプロイ出来る
      • Layerでお気に入りの依存関係を作っておけば使いまわせる
        • Amazon LinuxでビルドすればC拡張も入るっぽい
      • 管理不要
      • 従量課金でコストが安そう
    • cons
      • cold start時のタイムラグが気になる
      • zipでまとめるのが若干面倒+圧縮前250M制限あり
        • でかい辞書とかは入らなさそう(NEologdはダメそう)
      • ロックイン嫌な人は向いていない

です。

明日19日はtamurashingo氏がJavaについて話すらしいですよ?お楽しみに

記事が少しでもいいなと思ったらクラップを送ってみよう!
273
+1
@aflc'の技術ブログ

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする