Amazon Personalizeでやらかさないための使い方とLamdbaでの使用方法

# Amazon Personalize の一般提供を開始!
先日Amazon Personalizeの一般提供を開始しました。
プレビューの段階からお試しで使っていたので作成の一連の流れを説明していきます。
今回はユーザーに商品を推薦するモデルとエンドポイントの作成を行います。
`APIGateway`と`Lambda`を使用してAPIも作成しました。

[Amazon Personalize の一般提供を開始](https://aws.amazon.com/jp/about-aws/whats-new/2019/06/amazon-personalize-now-generally-available/)

# 課金体系(絶対に最初に目を通す！！！)
|  |料金  |
|---|---|
|データ取り込み  |0.05 USD/GB  |
|トレーニング  |0.24 USD/トレーニング時間  |
|レコメンデーション  |  |
|最初の 20K TPS-時間まで|0.20 USD/1 TPS-時間のリアルタイムレコメンデーション|
|次の 180K TPS-時間|0.10 USD/1 TPS-時間のリアルタイムレコメンデーション|
|200K TPS-時間以上|0.05 USD/1 TPS-時間のリアルタイムレコメンデーション|

## やらかしてしまった(大量のリクエストには気をつけて！！！)

後に記述しますが`boto3`を使用してレコメンドを取得します。
調子に乗って`API`を叩きまくってしまった結果がこれです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/2467ecb0-03b3-db82-38b5-c93378c5c6a0.png)

![pose_zetsubou_man.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/7c9da700-86e8-1f2c-b3b0-0aa487b44b83.png)
やらかしました。`3124.7$`って何円だっけ？(約33万円)思考停止です。

ユーザーがおよそ3万人いたのでどんな商品が推薦されるのか知りたくてこんなコードを書いたらなってしまいました。

```python
len(users)
# およそ3万
for user in users:
    recommend_item = personalizert.get_recommendations(
        campaignArn="<Campaign ARN>",
        userId=user['user_id'],)

personalizert = boto3.client(
    'personalize-runtime',
    region_name='ap-northeast-1')
```

のちに記述しますが、エンドポイント設定のTPSの上限値を低く設定して入れば回避できていました。（激しく後悔）



# 1. データを用意する
Amazon Personalizeにデータを読み込ませる際、S3にアップロードされたcsvファイルを使用します。
必要なデータ(ファイル)は以下の3つ([データセットの説明](https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/how-it-works-dataset.html))
私は`Redshift`からデータを取得し、`pandas`等を利用してcsvファイルを作成しました。

- ユーザーIDと商品IDが紐づいているデータ
- ユーザーIDにユーザー情報が紐づいているデータ
- 商品IDに商品情報が紐づいているデータ

それぞれ必須フィールドや予約キーワードがあります。



## ユーザーIDと商品IDが紐づいているデータの例
ユーザーID、商品ID、購入時刻のUNIX時間が格納されているファイル

```:user_item.csv
USER_ID,ITEM_ID, TIMESTAMP
582,456,1477897643
582,458,1477897643
582,490,1472037749
601,855,1472033257
601,451,1472033257
601,451,1472033257
602,454,1472033266
..................
```
`USER_ID`、`ITEM_ID`、`TIMESTAMP`が必須フィールドです。 予約語として`EVENT_TYPE`、`EVENT_VALUE`が用意されています。AWSのドキュメントを読んでみた感じでは,`EVENT_TYPE`は購入方法(ネット購入や店舗購入)などのデータ、`EVENT_VALUE`は商品の値段などのデータを入れるフィールドだと感じました。


## ユーザーIDにユーザー情報が紐づいているデータの例
ユーザーID、年齢、性別が格納されているファイル

```:user.csv
USER_ID,AGE,GENDER
180,71,male
185,55,female
332,30,female
352,51,male
365,22,female
..................
```
`USER_ID`が必須フィールドで残りの`AGE`と`GENDER`は[データセットの説明](https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/how-it-works-dataset.html)を真似しました。




## 商品IDに商品情報が紐づいているデータの例
商品ID、商品名、商品カテゴリーIDが格納されているファイル

```:item.csv
ITEM_ID,ITEM_NAEM,CATEGORY_ID
461,歯ブラシ(青),1
693,オールスパイス ,10
112,赤ワイン,9
113,白ワイン,9
369,キャンドル(10本入),6
867,三角帽,15
..................
```
`ITEM_ID`が必須フィールドで残りの`ITEM_NAEM`と`CATEGORY_ID`はこれも[データセットの説明](https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/how-it-works-dataset.html)をまねしてみました。フィールドを増やしても問題なさそうです。


# 2. S3にファイルをアップロードし、バケットポリシーを設定する
PersonalizeからS3にアクセスするためにバケットポリシーを作成します。

```json
{
    "Version": "2012-10-17",
    "Id": "PersonalizeS3Bucket AccessPolicy",
    "Statement": [
        {
            "Sid": "PersonalizeS3BucketAccessPolicy",
            "Effect": "Allow",
            "Principal": {
                "Service": "personalize.amazonaws.com"
            },
            "Action": [
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::<作成したバケット名>",
                "arn:aws:s3:::<作成したバケット名>/*"
            ]
        }
    ]
}
```

# 2.データをインポートする
作成したcsvファイルをS3にアップロードしたら、Personalizeのコンソール画面から`Dataset groups`を作成します。
<img width="1010" alt="スクリーンショット 2019-06-12 14.13.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/7f7e29e3-5dc0-214e-1382-1e1076a9c9ce.png">

csvファイルのヘッダ名と同じように`Schema definition`を編集します。
<img width="1001" alt="スクリーンショット 2019-06-12 14.25.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/53455aa5-c881-fa4c-ab1a-a85aa2ba83cc.png">

`IAM service role`は今回は既存の物を使用しましたが、ここでも作成できます。
`Data location`にS3のパスを書いてインポート開始です。
<img width="1003" alt="スクリーンショット 2019-06-12 14.29.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/fd9fa007-4cba-bd11-010f-4c0a0d02302c.png">

ダッシュボードをみると`Create in progress`になっています。
この調子で`User data`と`Item data`も作成していきます。
<img width="978" alt="スクリーンショット 2019-06-12 14.35.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/d06befa5-0d34-275e-4fc3-3905c5f3a948.png">

User dataを作成
<img width="1006" alt="スクリーンショット 2019-06-12 14.57.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/bf62067c-41f0-334b-4b7f-de7b033f5863.png">

Item dataを作成
<img width="1024" alt="スクリーンショット 2019-06-12 14.53.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/0c470dc0-f090-14c6-c4db-732842f4138f.png">

# 3. solutionsを作成する
データのインポートが全て終了したらsolutionsを作成します。
<img width="2183" alt="スクリーンショット 2019-06-12 15.07.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/83d63dbf-5173-06bf-a2ac-7deb63a5e806.png">


`Automatic(AutoML)`にチェックを入れるとインポートしたデータから適したモデルを作成します。
割と時間かかりました(1時間以上はかかった気がします)。
[モデルをトレーニングするためのレシピの一覧](https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/working-with-predefined-recipes.html)
今回は`aws-hrnn`と`aws-hrnn-metadata`を使用していきます。
<img width="724" alt="スクリーンショット 2019-06-12 15.11.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/50c849d7-e4e1-b10d-e8a6-81728d7b7e16.png">


# 4. campaignを作成する
campaignの名前、solutionを設定、スループットの下限を設定して作成(30分くらい)。

(<font color="Red">重要!</font>)スループットの下限はなるべく少なくした方がいいです。DynamoDBのキャパシティーと同じ感じです。私はこの値をテキトーに`200`にして、エンドポイントを叩きまくってしまった結果高額な請求がきてしまいました。

<img width="718" alt="スクリーンショット 2019-06-12 16.20.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/5c7b6122-317d-cac9-69bb-8538647f8f6a.png">

# 5. レコメンドしてみる
campaignの作成が完了するとレコメンドのテストがコンソール画面上から行えるので適当なユーザーIDでテストしてみます。
<img width="719" alt="スクリーンショット 2019-06-12 16.35.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166626/fbc1ebdb-07bb-5258-5818-6866dd18e2ec.png">
商品IDが複数推薦されます。画面では表示しきれてないですが25件推薦されました。

# 6. boto3から実行する
とりあえずローカルの環境から取得できるか、試してみました。

```python:main.py
import boto3

if __name__ == "__main__":

    personalizert = boto3.client('personalize-runtime', region_name='ap-northeast-1')

    response = personalizert.get_recommendations(
        campaignArn="<Campaign ARN>",
        userId='<User ID>',)

    for item in response['itemList']:
            print (item['itemId'])
```

## 注意点
ローカルのaws-cliに`personalize-runtime`がない場合があるのでサービスを追加する必要があります。
各サービスのjsonをダウンロードしてaws-cliにセットするだけなのですぐできました。

[AWS CLI のセットアップ](https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/aws-personalize-set-up-aws-cli.html)


# 7. APIを作成する
`SeverlessFrameWork`を使用してAPIを作成しました。

## コードなど

```yml:serverless.yml
service: personalize-test

provider:
  name: aws
  runtime: python3.7
  stage: stg
  region: ap-northeast-1
  profile: test

functions:
  recommend:
    handler: src/handler.get_recommend
    events:
      - http: GET /{user_id}
```

```python:handler.py
import json
import boto3


def get_recommend(event, context):
    user_id = event['pathParameters']['user_id']
    personalizert = boto3.client('personalize-runtime', region_name='ap-northeast-1')

    response=personalizert.get_recommendations(
        campaignArn="<Campaign ARN>",
        userId=str(user_id),)
        
    item_list = []

    for item in response['itemList']:
        item_list.append(item['itemId'])
    
    body = {
        'item_list': item_list
    }
    
    response = {
        'statusCode': 200,
        'isBase64Encoded': False,
        'headers': {'Content-Type': 'application/json'},
        'body': json.dumps(body)
    }

    return response

```

## 問題発生

`sls deploy`でデプロイをしたらAPIのエンドポイントが表示されるので`curl`で試してみたところ`Internal Server Error`が返って来ました。
CloudWatchのログを見た所以下のログが出ました。


```
[ERROR] UnknownServiceError: Unknown service: 'personalize-runtime'. .....
```
内容としては`boto3`で`personalize-runtime`というサービスはないとのこと。githubの[issue](https://github.com/boto/boto3/issues/1858)にもあがっていました。
解決策としては`amazon-linux`上で最新の`boto3`をインストールし、zipで固めてLamdbaのLayerとしてboto3を使用するとのことでした。
AmazonPersonalizeはつい最近一般公開されたばかりでLambdaのboto3の対応が間に合っていないんですかね。

以前の記事([pandasをLambdaのLayerとして追加する](https://qiita.com/thimi0412/items/4c725ec2b26aef59e5bd))で似たようなことを行なったので同様にzipファイルを作成しました。
作成したzipファイルは[github](https://github.com/thimi0412/lambda_layer_boto3)にあげましたので使用される人がいれば使ってみてください。

## いざ再び

```
$ curl https://XXXXXXX.execute-api.ap-northeast-1.amazonaws.com/stg/242 | jq 
```
```
{
  "item_list": [
    "2200399000000",
    "4580118279049",
    "4580168655268",
    "4580216370631",
    "4571243110208",
    "4512442005292",
    "2400040256128",
    "4954540129232",
    "2430002732278",
    "4512442005490",
    "4982757911630",
    "4997640091030",
    "4997640092105",
    "4512442005179",
    "4976631605029",
    "2400040918354",
    "2401020995907",
    "4536361026251",
    "2430002792463",
    "4573222000029",
    "2430000383120",
    "4580127310146",
    "4571243110130",
    "4539427300012",
    "2430000362002"
  ]
}
```

返ってきたー！これで既存のアプリケーションへの利用もできそうです。

#終わりに
今回で`Amazon Personalize`の課金体系を身をもって感じたのでみなさんも気をつけてください。 それでも`Amazon Personalize`は簡単にレコメーンデーションを作成するとても素晴らしい`AIサービス`なのでご利用は計画的に。
今回は`Event trackers`は使用しませんでしたが、`Event trackers`を使用すればリアルタイムにレコメンデーションをすることができるので知見がたまり次第紹介しようと思っています。

[ユーザーインタラクションイベントの記録](https://docs.aws.amazon.com/ja_jp/personalize/latest/dg/recording-events.html)