AWS Lambda で画像AIを動かす

## はじめに

AWS Lambda で Pytorch の画像AIを動かしてみました

SAM CLI で簡単に構築できるので、ちょっとした AIサービスなら低コストで実現できそうです

## 実行環境

- macOS Big Sur 11.4
- SAM CLI 1.24.1
- Docker 20.10.6

## 参考にしたもの

2021/2/12 のブログです

こちらは自然言語処理のAIを動かしています

https://aws.amazon.com/jp/blogs/machine-learning/using-container-images-to-run-pytorch-models-in-aws-lambda/

## AWS Lambda とは

AWS Lambda　はサーバレスコンピューティングサービスです

https://aws.amazon.com/jp/lambda/

開発したコードをアップロードすると、呼び出されたときだけマシンが起動し、コードを実行して、自動的に終了してくれます

何が嬉しいかというと

- サーバーがないので、サーバー構築や保守が必要ない
- マシンが起動した時間とマシンに割り当てたメモリで課金されるため、使っていない間はコストがかからない
- 大量のリクエストを受けると必要な数だけマシンが立ち上がる
- コードをアップロードし直すだけで無停止のシステム更新が可能

つまり、低コストで高可用性のシステムが構築できるわけです

ただし、万能ではありません

- マシン起動時に時間がかかる

    遅延してもいいようにするか、常時1台はマシンが起動しているようにする

- 大量アクセスで思わぬ高コストになることもあり得る

    認証、アクセス制限、API　Gatewayによる使用量制限などを入れる

## SAM とは

SAM = サーバーレスアプリケーションモデル

Lambda や API Gateway など、サーバレスアーキテクチャの構築を支援してくれます

インストール方法はこちら

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html

昔、むかーしむかし、 Lambda の開発には Apex というツールを使っていました

Lambda のデプロイのややこしいところを上手くラッピングしてくれていたからです

しかし、2018年に開発が停止し、リポジトリーもアーカイブされ、インストールするためのリリースも消されました

https://github.com/apex/apex

今では Lambda 以外もまとめて Terraform 管理にしています

https://www.terraform.io/

なので、実のところ SAM を触るのは初めてでした

## Lambda の新機能

少し寄り道して、 Lambda で AI を動かせたのが嬉しかった理由を書いておきます

かつて、 AWS Lambda はコードを Web コンソールから直接入力するか、 Zip に固めてアップロードするしかありませんでした

必要なパッケージがある場合、パッケージも Zip に含める必要があります

しかし、 Pytorch は非常に大きいパッケージで、 Zip によるアップロードの上限を超えます

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html

Zip 圧縮して 50MB が上限では、とても使えません

昔も画像AIを Lambda で動かしたいと思ったことがありましたが、この容量の問題で断念していました

ところが、 2020年12月、とても素晴らしい新機能が追加されたのです

https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/

なんと、コンテナイメージが動かせるようになったのです！

もう何でもありじゃないか、、、

自分で好きなパッケージを好き放題入れて動かせます！

AIでも何でもバッチコイです！

## プロジェクト作成

何はともあれ、 SAM を動かしてみましょう

まず、以下のコマンドで対話しながらプロジェクトの大枠を作ります

```bash
sam init
```

実行すると、以下のように表示されます

```bash
Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice:
```

今回は `1 - AWS Quick Start Templates` を選択します

続いて、どのようにコードをアップロードするか聞かれます

```bash
What package type would you like to use?
	1 - Zip (artifact is a zip uploaded to S3)
	2 - Image (artifact is an image uploaded to an ECR image repository)
Package type:
```

当然、 `2 - Image` を選びます

続いてベースになるイメージの選択です

```bash
Which base image would you like to use?
	1 - amazon/nodejs14.x-base
	2 - amazon/nodejs12.x-base
	3 - amazon/nodejs10.x-base
	4 - amazon/python3.8-base
	5 - amazon/python3.7-base
	6 - amazon/python3.6-base
	7 - amazon/python2.7-base
	8 - amazon/ruby2.7-base
	9 - amazon/ruby2.5-base
	10 - amazon/go1.x-base
	11 - amazon/java11-base
	12 - amazon/java8.al2-base
	13 - amazon/java8-base
	14 - amazon/dotnet5.0-base
	15 - amazon/dotnetcore3.1-base
	16 - amazon/dotnetcore2.1-base
Base image:
```

Python で動かしたいので、 `4 - amazon/python3.8-base` を選びます

プロジェクト名は適当に入れてください

```bash
Project name [sam-app]:
```

今回は `py-sam-app` とします

次に、テンプレートを選びます

```bash
Cloning from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
	1 - Hello World Lambda Image Example
	2 - PyTorch Machine Learning Inference API
	3 - Scikit-learn Machine Learning Inference API
	4 - Tensorflow Machine Learning Inference API
	5 - XGBoost Machine Learning Inference API
Template selection:
```

`2 - PyTorch Machine Learning Inference API` を選べば、それだけで AI システムの大枠が作られます

もちろん、用途に応じて `Tensorflow` 等を選んでも OK です

以下の表示が出れば、必要なコードは揃っています

```bash
    -----------------------
    Generating application:
    -----------------------
    Name: py-sam-app
    Base Image: amazon/python3.8-base
    Dependency Manager: pip
    Output Directory: .

    Next steps can be found in the README file at ./py-sam-app/README.md
```

## ビルド

Lambda をコンテナで動かすので、当然コンテナをビルドする必要があります

あらかじめ、自分の IAM ユーザーに以下のサービスを自由に使える権限があることを確認してください

なければ付与してください

- Lambda
- API Gateway
- ECR
- IAM
- CloudFormation

まず、 ECR にコンテナ用のリポジトリーを作ります

```bash
aws ecr create-repository \
  --repository-name py-sam-app \
  --image-scanning-configuration \
  scanOnPush=true
```

以下のようなレスポンスが返ってきます

```json
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:<REGION>:<AWS_ACCOUNT_ID>:repository/py-sam-app",
        "registryId": "<AWS_ACCOUNT_ID>",
        "repositoryName": "py-sam-app",
        "repositoryUri": "<AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/py-sam-app",
        "createdAt": 1625620019.0,
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": true
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}
```

続いて、リポジトリーにプッシュするために ECR にログインしておきます

`<repositoryUri>` は上記 ECR リポジトリー作成のレスポンスに入っています

```bash
aws ecr get-login-password | docker login \
  --username AWS \
  --password-stdin <repositoryUri>
```

`Login Succeeded` とレスポンスがあれば OK です

続いて、作成したプロジェクトのディレクトリーに移動し、ビルドします

```bash
cd py-sam-app \
  && sam build
```

内部で `docker build` が実行され、最終的に以下のように結果が表示されます

```bash
Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
```

では、ローカルで一度動かしてみましょう

```bash
sam local invoke InferenceFunction --event events/event.json
```

以下のように結果が返ってくれば、 AI モデルが動かせています

```bash
Invoking Container created from inferencefunction:python3.8-v1
Building image.........
Skip pulling image and use local one: inferencefunction:rapid-1.24.1.

START RequestId: f034abb7-2e1f-4c44-a754-ee67be8fb419 Version: $LATEST
END RequestId: f034abb7-2e1f-4c44-a754-ee67be8fb419
REPORT RequestId: f034abb7-2e1f-4c44-a754-ee67be8fb419  Init Duration: 2.00 ms  Duration: 1577.01 ms Billed Duration: 1600 ms        Memory Size: 5000 MB    Max Memory Used: 5000 MB
{"statusCode": 200, "body": "{\"predicted_label\": 3}"}%
```

少し解説します

プロジェクトディレクトリー内の `template.yml` を開くと、以下のように記述されています

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 50
    MemorySize: 5000
  Api:
    BinaryMediaTypes:
      - image/png
      - image/jpg
      - image/jpeg

Resources:
  InferenceFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      PackageType: Image
      Events:
        Inference:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /classify_digit
            Method: post
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./app
      DockerTag: python3.8-v1
...
```

ここで、どういう API で、どういう関数で、といったことが定義されているわけです

この中の `InferenceFunction` を先ほど指定して実行しました

引数に指定した `events/event.json` を見てみましょう

色々と書かれていますが、一番下に関数に渡す値が定義されています

```json
...
  },
  "body": "iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAAAAABXZoBIAAABQGlDQ1BJQ0..."
}
```

関数の中身を少し見てみます

関数は `app/app.py` に定義されています

```python
...
def lambda_handler(event, context):
    image_bytes = event['body'].encode('utf-8')
    image = Image.open(BytesIO(base64.b64decode(image_bytes))).convert(mode='L')
    image = image.resize((28, 28))

    probabilities = model.forward(image_transforms(np.array(image)).reshape(-1, 1, 28, 28))
    label = torch.argmax(probabilities).item()
...
```

`event` から `body` を取り出し、 BASE64 デコードしていますね

つまり、入力となるデータは BASE64 エンコードされた画像データということがわかります

試しに以下のサイトで `body` の中の文字列をデコードしてみましょう

https://rakko.tools/tools/71/

デコード結果は数字の 3 の手書き文字でした

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7a254d66-23d9-61b4-436a-245945d2e7a0.png)

MNIST dataset のようですね

http://yann.lecun.com/exdb/mnist/

つまり、このアプリは手書き数字の 0 から 9 を判別する画像 AI を動かしているわけです

## デプロイ

では、ビルドしたものをクラウド上にデプロイしましょう

`init` のときと同じく、対話型で進めます

```bash
sam deploy --guided
```

まず、スタック名を入力します

```bash
Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: 
```

SAM は内部で AWS CloudFormation を動かしています

https://aws.amazon.com/jp/cloudformation/

スタックとは、一連の関連するリソースをまとめたものです

今回で言うと、 Lambda 関数や API Gateway 、 IAM ロールなどです

これらをスタックとしてまとめて実行するため、名前をつけます

今回はここも `py-sam-app` としておきます

続いてリージョンを指定します（デフォルトで問題ありません）

```bash
        AWS Region [ap-northeast-1]:
```

次に ECR　のリポジトリーを聞かれるので、作ったリポジトリーのURL（前出の `repositoryUri` ）を入力します

```bash
        Image Repository for InferenceFunction: 
```

デプロイ前に確認するかと聞かれますが、今回は `n` で良いでしょう

```bash
        #Shows you resources changes to be deployed and require a 'y' to initiate deploy
        Confirm changes before deploy [y/N]: 
```

SAM が IAM ロールを作るのを許可するかと聞かれるので、もちろん `y` と答えます

```bash
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: 
```

API に認証がかかってないけど大丈夫？と心配してくれます

業務で使う場合はもちろん認証が必要ですが、今回はお試しなので `y` です

```bash
        InferenceFunction may not have authorization defined, Is this okay? [y/N]: 
```

設定を保存するか聞かれます

保存しておけば、次回以降、今回の内容がデフォルト値になるため、 `y` です

```bash
        Save arguments to configuration file [Y/n]: 
```

設定ファイル名はデフォルトのままで良いでしょう

```bash
        SAM configuration file [samconfig.toml]: 
```

環境もデフォルトにします

```bash
        SAM configuration environment [default]: 
```

これで ECR へのプッシュが始まり、続けて各リソースが作られていきます

最後に成功のメッセージが表示されます

```
Successfully created/updated stack - py-sam-app in ap-northeast-1
```

## 実行

上記成功メッセージの上に、 API Gateway の作成結果が表示されています

```bash
Key                 InferenceApi                                                                
Description         API Gateway endpoint URL for Prod stage for Inference function              
Value               https://xxx.execute-api.ap-                                          
northeast-1.amazonaws.com/Prod/classify_digit/
```

この Value のところが作成された API のエンドポイントなので、ここに向けて BASE64 の文字列（event/event.json の body の値）を投げてみましょう

```bash
curl -XPOST https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prod/classify_digit/ \
  -d "iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAAAAABXZoBIAAABQGlDQ1BJQ0..."
```

最初はすごく時間が掛かって、もしかしたらタイムアウトするかもしれません

```bash
{"message": "Endpoint request timed out"}% 
```

これは、 初回呼び出し時にマシンを起動しているのと、モデルの読み込みを行っているためです

本番ではタイムアウト時間を調節するなどしてください

もう一回呼んでみましょう

すると、正しい結果が返ってきます

```bash
{"predicted_label": 3}% 
```

ちゃんと 3 が返ってきましたね！

## 注意

お試しで作った関数、API　Gateway、ECRリポジトリー、IAMユーザーは削除しておきましょう

API　を攻撃されたら高額請求が来るかも

## 最後に

あとは、 Dockerfile や app.py を編集して、自分の使いたいモデルを組み込めば何でも動かせます

私は物体検出を動かしてみましたが、無事動作できました

ただし、 `template.yml` の `MemorySize` に書いてあるように、メモリを 5,000 MB 割り当てています

実際、物体検出を動かした時は 3,000 MB 以上使われていました

これは結構高いので、多数アクセスがある場合、結局割高になりそうです

GPU を積んでいないため、処理に時間がかかる点も考慮しましょう

アクセスが多く、速度を求められるような場合、やはり Lambda は向いていないでしょう

とはいえ、これを活かせる場面もあると思うので、今後の選択肢に入れておきます