リモート開発メインのソフトウェア開発企業のエンジニアブログです

機械学習の学習済みモデルを使用して API サーバーを構築する

やりたいこと

機械学習(ML)の学習済みのモデルを用いて、推論処理を行う API サーバーを構築する、というのが今回やりたいことです。

一般的に、学習には高機能な GPU インスタンスが必要なのに対して、推論処理は、CPU インスタンス、あるいは小さめの GPU インスタンスでも問題無く動作します。従って、高性能インスタンスで学習したモデルを、比較的性能の低いインスタンスに移して動作させるというのが一般的です。

今回のタスクは画像分類ですが、他の機械学習でもほぼ同じように出来ると思います。

環境

今回は以下の環境を用いました。

  • AWS Deep Learning AMI (Amazon Linux ベースのもの)を使用
  • ML フレームワークは Caffe
  • NVIDIA DIGITS 上で学習したモデルを使用
  • Web フレームワークには Flask
  • Web server は Apache + mod_wsgi

ただ、過去の自分にアドバイス出来るのであれば、以下の構成を使うかなと思います。理由に関しては後述します。

  • AWS Deep Learning AMI は使用しないで、使用する ML フレームワーク(今回は Caffe)のみがインストールされたインスタンスを使用する
  • Amazon Linux ベースでは無く Ubuntu ベースの AMI を使用し、Apache + mod_wsgi ではなく、Gunicorn などのスタンドアロンサーバーか、nginx + uWSGI などを使用する
Moba Pro

やったこと

Flask で推論処理の API サーバーを実装

API サーバーの実装に、今回は Flask を使用しました。参考にしたのは以下のページです。

Deploying a Machine Learning Model as a REST API – Towards Data Science

ただ、これも後から気づいたんですが、上のページのコードは若干冗長なんで、Flask のドキュメントも見ておくと良いと思います。

Quickstart — Flask 1.0.2 documentation

出来たコードとしてはこんな感じです。本筋と関係ない部分で削られている箇所もありますので、ご注意下さい。なお、説明をコメントに記載してあります。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# このファイルは api_server.py として保存

# Flask 関連の import
from flask import Flask
from flask_restful import reqparse, abort, Api, Resource

# Caffe の import
import caffe

# その他 import
import os.path
import numpy as np
import sys

img_dir = '/path/to/image-dir'
model_dir = '/path/to/model-dir'
model_file = 'snapshot_iter.caffemodel'

# AWS DL AMI に載ってる Caffe は、CPU モードが使えない
# https://docs.aws.amazon.com/ja_jp/dlami/latest/devguide/cpu.html
caffe.set_mode_gpu()

# モデルの読み込み
net = caffe.Net(model_dir + '/deploy.prototxt',
model_dir + '/' + model_file,
caffe.TEST)

# transformer の作成(詳細は Caffe のチュートリアルとかを参照)
transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape})
transformer.set_transpose('data', (2,0,1))
transformer.set_raw_scale('data', 255) # the reference model operates on images in [0,255] range instead of [0,1]
transformer.set_channel_swap('data', (2,1,0)) # the reference model has channels in BGR order instead of RGB

# API の引数として、filename を受け取るように設定
parser = reqparse.RequestParser()
parser.add_argument('filename')

# API のメイン処理
class ClassifyImage(Resource):
    def get(self):
        # ファイル名を取得
        args = parser.parse_args()
        filename = args['filename']
        file_path = img_dir + '/' + filename

        if not os.path.isfile(file_path):
            return {'result': 'error', 'message': '{} not found'.format(file_path)}

        # 入力データの設定
        net.blobs['data'].data[...] = transformer.preprocess('data', caffe.io.load_image(file_path))
        # 推論
        out = net.forward()

        # 推論結果のベクトル
        a = out['softmax'][0]

        # 結果の JSON object を生成
        output = {'result': 'ok', 'prediction': a.argmax()}

        return output

# Flask の初期化みたいなの
app = Flask(__name__)
api = Api(app)

# / にアクセスされたら、ClassifyImage.get を実行するように設定
# 最近は以下のようにやるのが普通らしいが、今回は参考元のページに倣った
# @app.route('/')
api.add_resource(ClassifyImage, '/')

# コマンドラインから実行されたときに限り、実行する
if __name__ == '__main__':
    app.run(debug=True)

ローカルでのテスト方法としては、以下の通りコマンドラインよりスタンドアロンのサーバーを起動できます。デフォルトでは localhost:5000 で LISTEN します。

./api-server.py

後は、 curl コマンドなどでテストしてみて下さい。

curl http://localhost:5000/?filename=foo.jpg

ローカルのテストが完了したら、本番環境でも動作させたいところですが、デプロイの選択肢としてはいくつか考えられます。今回は mod_wsgi を選択しましたが、詳細は以下のページをご参照下さい。

Deployment Options — Flask 1.0.2 documentation

mod_wsgi の設定

API サーバーが動作するようになったので、次はそれを mod_wsgi 経由で動かすようにします。

基本的な設定方法

最初に、以下のような *.wsgi ファイルを作成します。

# foo-api.wsgi
import sys, os

import logging
# to output to Apache log
logging.basicConfig(stream = sys.stderr)

sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))

from api_server import app as application

Apache 関連ですが、最初は mod_wsgi をインストールします。

# Amazon Linux 等、Red Hat 系の場合
sudo yum install mod_wsgi-python27
# Debian, Ubuntu 等
sudo apt-get install libapache2-mod-wsgi

設定ファイルは以下のようになります。

Listen 5000

WSGIDaemonProcess foo-api user=ec2-user group=apache threads=1
WSGIScriptAlias / /var/www/foo-api/foo-api.wsgi

WSGIProcessGroup foo-api
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all

あとは、Apache を再起動すれば、5000番ポートで API サーバーが待ち受けるようになります。

Anaconda 環境の Python を使用

ここまでの方法だと、システム環境の Python であれば問題無いんですが、AWS DL AMI の場合、各フレームワークの環境は Anaconda を使って切り替えて使用するようになっています。従って、もう一手間必要です。

色々調べたんですが、

  • python-path をいじれば大丈夫
  • mod_wsgi を手動でインストールしないとダメ

という2つの意見がありましたが、結論としては、(少なくとも自分の場合は)mod_wsgi を手動でインストールしないとだめでした。以下の Stack Overflow の質問も参考にしてみて下さい。

では、具体的な方法を説明します。

まず、mod_wsgi などを pip でインストールし、パッケージで入れたものは削除します。

# Caffe in Python 2.7 の環境に切り替える
source activate caffe_p27

# flask はインストール済みっぽいので、flask-restful のみインストール
pip install flask-restful

# 既存の mod_wsgi をアンインストール
sudo yum remove mod_wsgi-python27

# mod_wsgi のビルドに必要なのでインストール
sudo yum install httpd-devel
# mod_wsgi をインストール
pip install mod_wsgi

次に、mod_wsgi の Apache モジュールをインストールします。

# root じゃないとインストールできませんが、caffe_p27 環境は
# ec2-user にしか存在しないので
sudo -i
export PATH=/home/ec2-user/anaconda3/bin/:$PATH
source activate caffe_p27

# モジュールのインストール
mod_wsgi-express install-module
exit

Apache の設定ファイルを以下の通り修正します。

Listen 5000

LoadModule wsgi_module "/usr/lib64/httpd/modules/mod_wsgi-py27.so"
WSGIPythonHome "/home/ec2-user/anaconda3/envs/caffe_p27"

WSGISocketPrefix /var/run/wsgi

WSGIDaemonProcess foo-api user=ec2-user group=apache threads=1
WSGIScriptAlias / /var/www/foo-api/foo-api.wsgi

WSGIProcessGroup foo-api
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all

これに関しては、古いですが、以下のページが参考になりました。

virtualenv + flask + apache + wsgi で動かすまで – Qiita

これで良さそうな気もしますが、この状態だと、*.so が見つからないというエラーも出ました。

LD_LIBRARY_PATH を設定すれば良いのだろうと思ったのですが、 mod_wsgi の場合、新たにプロセスが作られるため、Apache のディレクティブ SetEnv で指定しても効きませんでした。結論としては、*.wsgi をいじって以下の行を入れました。

sys.path.insert(0, '/home/ec2-user/src/caffe_python_2/python')

参考にしたのは以下の gist です。

Setting environment variables for Apache/mod_wsgi hosted Python application.

パス等の設定に関して補足

PYTHON_HOME, PYTHON_PATH の設定の仕方として、WSGIPythonHome, WSGIPythonPath あるいは WSGIDaemonProcess の python-home などいくつか方法があるようです。

後方互換性とかその辺もあって色々ごちゃごちゃしているようなので、詳しくは以下のページを参考にして下さい。

他の選択肢

読んでもらったら分かる通り、今回やった方法はかなり面倒くさいと思いますので、ここでは他の選択肢を挙げます。

AWS DL AMI は使わない

一番面倒で時間がかかったのは、Anaconda の環境を mod_wsgi で動かす事でしたが、本番環境で複数のフレームワークを切り替えて使う必要はないため、システム環境の Python に直接 Caffe 等がインストールされている環境だとかなり楽を出来ると思います。

mod_wsgi を使わない

mod_wsgi はやっぱり色々面倒でしたので、以下のページに記載されている他の選択肢を使った方が簡単だったかもしれません。

Deployment Options — Flask 1.0.2 documentation

個人的には、次は uWSGI か Gunicorn を試してみようと思います。

Amazon Linux ベースの AMI を使わない

uWSGI や Gunicorn を使う際に、Amazon Linux ベースの AMI だとインストールが比較的面倒なので、色んなパッケージが充実している Ubuntu ベースの AMI の方が良いと思います。

Flask ではなく Bottle

知り合いに教えてもらったのですが、Python の軽量フレームワークで Bottle というのがあるそうで、こっちの方が Flask よりもさらに簡潔に見えます。次回はこっちを使ってみたいと思います。

Bottle: Python Web Framework — Bottle 0.13-dev documentation

まとめ

今回、紆余曲折を経ながらも、Caffe で学習済みのモデルを使用した、推論処理を行う API サーバーを構築することが出来ました。

機械学習フレームワークは Python 製のものが大半なので、Flask あるいは Bottle の使い方を覚えておくと、簡単に API サーバーが構築できると思います。

← 前の投稿

React Nativeのライブラリをpatch-packageで手軽に修正する方法

次の投稿 →

AWS Lambdaを使ってスプラトゥーン2の戦績をstat.inkに定期保存できるようにした

コメントを残す