盆暗の学習記録

データサイエンス ,エンジニアリング,ビジネスについて日々学んだことの備忘録としていく予定です。初心者であり独学なので内容には誤りが含まれる可能性が大いにあります。

機械学習モデルを動かすWeb APIを作ってみる(5):Lambdaにデプロイ

これまで,APIの作成からHerokuへのデプロイまでを扱ってきました。 今回はAWS Lambda + API Gatewayでのデプロイについてメモしておきます。

前回はchaliceというアプリの作成&Lambda + API Gatewayへのデプロイを楽にできるライブラリを扱いました。

しかし,以下に述べるLambdaをそのまま弄る方法のほうが正直chaliceより楽かなという印象です。

Lambda関数を作成

AWS コンソールから作成します。

LambdaのLayerにパッケージを追加する

ソースコードと一緒に直接アップロードできるのは50MBまでなので,NumpyやらScikit-learnやらを使っている今回のコードはその制約をオーバーしてしまいます。

しかし,Layerとして別口でアップロードしておき,Lambda関数から読み込ませるようにする場合は250MBまで読み込めます。以下ではLayerを作っていきます。(ここは前回と同じです)

Amazon Linux2 OSのEC2インスタンスを立ち上げ,sshで接続する

pip installしてzipにする

# python3のインストール
sudo yum update -y
sudo yum install python3-devel python3-libs python3-setuptools -y
sudo python3 -m pip install --upgrade pip

# numpyのzip化 -------------
mkdir python
pip3 install -t ./python/ numpy
zip -r numpy.zip python
rm -r python

# scipyのzip化 -------------
mkdir python
pip3 install -t ./python/ scipy
# numpyは別Layerにするので消す
rm -r ./python/numpy*
zip -r scipy.zip python
rm -r python

# pandasのzip化 ------------
mkdir python
pip3 install -t ./python/ pandas==0.25.3
# numpyは別Layerにするので消す
rm -r ./python/numpy*
zip -r pandas.zip python
rm -r python

# sklearnのzip化 -----------
mkdir python
pip3 install -t ./python/ sklearn
# numpy, scipyは別Layerにするので消す
rm -r ./python/numpy* ./python/scipy*
zip -r sklearn.zip python
rm -r python

# lightgbmのzip化 ----------
mkdir python
pip3 install -t ./python/ --no-deps lightgbm
zip -r lightgbm.zip python
rm -r python

zipをダウンロード

exit

でEC2を抜けたら

scp -i ~/.ssh/[pemファイル名].pem ec2-user@[IPv4]:~/*.zip .

のような感じでローカルにダウンロードします。

zipをアップロード

  • AWSコンソール→Lambda→Layer→「レイヤーの作成」からzipをアップロードします

    • レイヤー名はpythonコードでimportするときの名前にします

Layerの追加

AWSコンソールのLambda関数の画面から,作成中のAPIの関数に先程作成したLayerを追加します。

(マージ順序は気にしなくて大丈夫です)

f:id:nigimitama:20200307234542p:plain

APIのコードを修正

Lambda用に修正してアップロードします。

コードの書き換え

前回までのAPIのコードを,こんな感じに書き換えます。

import pandas as pd
import json
import pickle
from datetime import datetime
import sys
sys.path.append("./modules")  # 前処理で使った自作モジュール「pipeline」を読み込むためPYTHONPATHに追加

# アプリ起動時に前処理パイプラインと予測モデルを読み込んでおく
preprocess = pickle.load(open("modules/preprocess.pkl", "rb"))
model = pickle.load(open("modules/model.pkl", "rb"))


def predict(event, context):
    """リクエストされたら予測値を返す関数"""
    try:
        # リクエストのbodyをjsonからdictに変換(API Gatewayのリクエスト形式に対応)
        data = json.loads(event['body'])
        # APIにJSON形式で送信された特徴量
        X = pd.DataFrame(data, index=[0])
        # 特徴量を追加
        X["trade_date"] = datetime.now()
        # 前処理
        X = preprocess.transform(X)
        # 予測
        y_pred = model.predict(X, num_iteration=model.best_iteration_)
        response = {"status": "OK", "predicted": y_pred[0]}
        # レスポンスもbodyにjsonを入れる(API Gatewayの仕様に対応)
        return {
            "body": json.dumps(response),
            "statusCode": 200
        }
    except Exception:
        response = {"status": "Error", "message": "Invalid Parameters"}
        return {
            "body": json.dumps(response),
            "statusCode": 400
        }

このようなreturnにする理由は,API Gatewayにレスポンスを指示するためです。

(参考:API ゲートウェイでの「不正な Lambda プロキシ応答」または 502 エラーの解決

zipファイルにしてアップロード

予測モデルの.pklファイルなどとともにzipにしてAWSコンソールからアップロードし,「保存」を押します。

Lambdaの設定

コードエディタのような画面がでてきたら「ハンドラ」部分をファイル名.関数名 の形にします。

f:id:nigimitama:20200307234650p:plain

また,その下に「基本設定」という部分があるので,メモリを引き上げておきます。

今回の例では200MB程度あれば十分のはずです。

f:id:nigimitama:20200307234711p:plain

API Gatewayの設定

REST APIにします

f:id:nigimitama:20200307234822p:plain

動作確認

AWSコンソール上でテスト

AWSコンソール上部でテストイベントを作成し,以下の値を入力します。

{
    "address": "東京都千代田区",
    "area": 30,
    "building_year": 2013
}

テストイベントを保存したら「テスト」のボタンを押してテストします。

以下のような画面になれば成功です。 f:id:nigimitama:20200307234854p:plain

ローカルからテスト

AWSコンソール上のAPI Gatewayの部分にAPIのエンドポイント(URL)とAPIキーがあるので,そこへPOSTリクエストを送ります。

import unittest
import requests
import json


class APITest(unittest.TestCase):
    URL = "APIエンドポイント"
    HEADERS = {"x-api-key": "APIキー"}
    DATA = {
        "address": "東京都千代田区",
        "area": 30,
        "building_year": 2013
    }

    def test_normal_input(self):
        # リクエストを投げる
        response = requests.post(self.URL, json=self.DATA, headers=HEADERS)
        # 結果
        print(response.text)  # 本来は不要だが,確認用
        result = json.loads(response.text)  # JSONをdictに変換
        # ステータスコードが200かどうか
        self.assertEqual(response.status_code, 200)
        # statusはOKかどうか
        self.assertEqual(result["status"], "OK")
        # 非負の予測値があるかどうか
        self.assertTrue(0 <= result["predicted"])


if __name__ == "__main__":
    unittest.main()

実行したところ,ちゃんと動きました

{"status": "OK", "predicted": 45833222.1903707}
.
----------------------------------------------------------------------
Ran 1 test in 0.398s

OK

もちろんcurlコマンドでも検証できます。

curl https://xxxxxxxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/test \
-d '{"address": "東京都千代田区", "area": 30, "building_year": 2013}' \
-H 'x-api-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
-v
{"status": "OK", "predicted": 45833222.1903707}

ちゃんと機械学習モデルによる予測値が返ってくるのが確認できました。

今回はここまで。

今回使用したコード

Githubにおいてありますのでご関心のある方はご覧ください。

github.com