PHP から SageMaker の推論を実行したい

# TL;DR
-  Python から SageMaker の推論を行うことが多いと思いますが、フロントエンドが PHP の場合もあると思います。EC2 で実行される PHP から SageMaker の推論を実行する方法を書いてみました。
-  PHP だと numpy を扱えないので、画像だと Base64 とかにエンコードして送ります。 

# EC2 と SageMaker 間のデータ形式を決める

これは PHP に限らず必要な作業です。フロントエンドの方とML Engineer の方で決めましょう。

SageMaker は基本 Python なので便利な numpy を使いたくなるかもしれません。numpy だと TensorFlow, PyTorch, MXNet など各種モデルに簡単にデータを渡すことができます。一方、送る側が PHP だと、Python のライブラリである numpy でデータを送るのは難しいです。そこで、numpy ではなくて、PHP でも扱える形式、例えば json や画像の場合は Base64 などを使ってみましょう。ここでは EC2 にある画像を Base64 にエンコードして SageMaker に送ることを考えます。

# SageMaker のエンドポイントを作る

ここでは試しに GluonCV の Semantic segmentation の pretrained モデルである DeepLab のモデルをホストします。
デプロイまでの流れは Gist に上げました。
https://gist.github.com/harusametime/ac3050d25c47b23534784ce8fb09da6b

デプロイには推論用のスクリプトが必要で、ここにBase64の画像をどう扱うかも実装します。まずモデル読み込みの model_fn 関数の実装です。これは gluoncv の関数一発で呼び出せます。GPU が使えるインスタンスを想定しているので、CPU のみの場合は、mx.cpu() を ctx にしてくださいね。

```python
def model_fn(model_dir):
    net = gluoncv.model_zoo.get_deeplab_resnet50_ade(pretrained=True, ctx=mx.gpu())
    return net
```

さて次は、上記のモデルにデータを渡して予測する transform_fn の実装です。最初に base64 のデコードをしたら、data には画像ファイルが入りますので、PIL Image で開いて、numpy変換、mxnet array へ変換します。DeepLab の前処理の test_transformを実行してから、predict で予測する流れです。Semantic Segmentation なので、ピクセルごとにラベルの候補を複数出してきます。ここでは一番尤もらしいラベルを出すとして、argmax で確率が最大のものを出します。最後に json 形式でレスポンスを返します。

```python
def transform_fn(net, data, input_content_type, output_content_type):
    
    data = base64.b64decode(data)
    data = Image.open(BytesIO(data))
    data = np.array(data)
    data = mx.nd.array(data,ctx=mx.gpu())
    data = test_transform(data,ctx=mx.gpu())
    output = net.predict(data)
    predict = mx.nd.squeeze(mx.nd.argmax(output, 1)).asnumpy().tolist()
    response_body = json.dumps(predict)
    return response_body, output_content_type
```

# EC2側の PHP 実装

PHPを触るのは15年ぶりくらいな気がしますがやっていきましょう。SageMaker の推論を Python から実行する際に Python SDK (Boto3) が必要なように、PHPには AWS SDK for PHP が必要です。

## EC2 から SageMaker の推論ができるよう IAM Role を付与

IAM Role の新規作成画面から EC2 を選んで、EC2 で使えるロールを作ります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/408ab122-7901-ebe9-22eb-9ba80366dd20.png">

そのロールから SageMaker を操作できるよう AmazonSageMakerFullAccess のポリシーを付与します。あとは名前を付けて保存しましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/a13124e5-4cef-523b-c2d2-6f5de1e45a17.png">

保存したロールはEC2の画面でインスタンスに付与できます。
![pic3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/b3153a9e-d7a0-9302-e488-13d987ea6670.png)




## AWS SDK for PHP のインストール
[公式のインストールガイド](https://docs.aws.amazon.com/ja_jp/sdk-for-php/v3/developer-guide/getting-started_installation.html)を参考にしましょう。
まず、PHPのバージョンが 5.5 以降が推奨です。私は使っている環境が PHP 5.3 だったのでバージョンを上げました。php をアンインストールして再インストールするだけです。もっとスマートなやり方があるかもしれません。php-xml はAWS SDK に必要だったのでいれました。

```shell
yum -y remove php-*
yum -y remove httpd-tools
yum clean all
yum -y install php
yum -y install php-xml
```

php が入ったら次は Composer のインストールです。[インストールガイド](https://getcomposer.org/download/) に従えば良いですが、私はガイドからリンクされている[How do I install Composer programmatically?](https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md) のファイルを作ってシェルから実行しました。

Composer が入ったら、いよいよ AWS SDK のインストールです。

```shell
composer require aws/aws-sdk-php
```

これを実行するとインストール完了です。実行したディレクトリに `vendor` というフォルダができて、そこには `autoload.php` というファイルができています。このファイルを PHP のスクリプトから呼べば、AWS SDK を全部読んでくれるようです。

## PHPのスクリプトを実行

推論を実行する際は InvokeEndpoint を実行しますので、AWS SDK for PHP のドキュメントから使い方を探します。

https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-runtime.sagemaker-2017-05-13.html

```php

$result = $client->invokeEndpoint([
    'Accept' => '<string>',
    'Body' => <string || resource || Psr\Http\Message\StreamInterface>, // REQUIRED
    'ContentType' => '<string>',
    'CustomAttributes' => '<string>',
    'EndpointName' => '<string>', // REQUIRED
    'InferenceId' => '<string>',
    'TargetModel' => '<string>',
    'TargetVariant' => '<string>',
]);
```
REQUIRED なのは `Body` と `EndpointName` ですね。Body には Base64画像を入れて、EndpointName には上の方で作成したエンドポイントの名前を入れます。わからない方は、SageMaker のコンソールから確認できます。
<div align="center">
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/7bbd2225-26a4-0f6c-75dc-65e0915f6fac.png">
</div>

では、このあたりをいれて php のコードを書いてみるとこんな感じです。

```php
<?php
require 'vendor/autoload.php';
use Aws\SageMakerRuntime\SageMakerRuntimeClient;

$im = file_get_contents("./image.jpg");
$base64img = base64_encode($im);
$client = new SageMakerRuntimeClient([
    'region'  => 'us-west-2',
        'version' => '2017-05-13'
]);

$result = $client->invokeEndpoint([
    'Body' => $base64img, // REQUIRED
    'EndpointName' => 'エンドポイントの名前', // REQUIRED
   ]);
print($result['Body']);
?>

```

実行してみるとこんな感じです。

```shell
[ec2-user@ip-10-0-0-144 ~]$ php predict.php
 [[5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0,
...(途中省略)
 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0,
3.0, 3.0, 3.0, 3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]
```

2次元配列なので画像の縦・横に対応していて、値としてラベル 5 とか 3 とか 0 が入っていますね。
ADE20K データセットでは、5 は天井、3 は床、0 は壁なので、与えた画像は室内の画像で、最初（上の方）に天井が写っていることが予測されますね。