タケユー・ウェブ日報

Webシステム受託会社の業務の中での気づきや調べごとのメモ。

Ruby による Amazon Personalize の使い方

AWS機械学習レコメンデーションサービスである Amazon Personalize を案件で利用したのでSDKの使い方メモです。

Amazon Personalize

aws.amazon.com

Amazon Personalize は、機械学習の知識がなくても、簡単にレコメンデーションをアプリに組み込むことができるサービスです。

これまで、AWS機械学習レコメンデーションを行うには、ElasticMapReduce + Mahoutで自前で計算するなど、インフラ構築と保守の手間があったり、モデル設計や評価についての知識が必要だったりとなかなかに大変だったのですが、一般的なレコメンデーションに限っては Amazon Personalize を使うことで、サーバーレスで簡単に実現できるようになりました。

この記事では Amazon Personalize の使い方については解説しません。公式ドキュメントその他を参照してください。

docs.aws.amazon.com

Ruby SDK

github.com

リソースの作成

データセットグループ

DATASET_GROUP_NAME = "my-first-datasetgroup"

personalize = Aws::Personalize::Client.new

dataset_group_arn = personalize.create_dataset_group(name: DATASET_GROUP_NAME).dataset_group_arn

スキーマ

ユーザー、アイテム、インタラクションのどのデータセットタイプなのかににより、適切な形式のスキーマを作成する。

docs.aws.amazon.com

SCHEMA_NAME = "Users"
DATASET_TYPE = "users" # or "items" or "interactions "

personalize = Aws::Personalize::Client.new

avro_schema = case DATASET_TYPE
when "users"
  {
    "type": "record",
    "name": "Users",
    "namespace": "com.amazonaws.personalize.schema",
    "fields": [
      {
        "name": "USER_ID",
        "type": "string"
      },
      {
        "name": "AGE",
        "type": "int"
      },
      {
        "name": "GENDER",
        "type": "string"
      }
    ],
    "version": "1.0"
  }
when "items"
  {
    "type": "record",
    "name": "Items",
    "namespace": "com.amazonaws.personalize.schema",
    "fields": [
      {
        "name": "ITEM_ID",
        "type": "string"
      },
      {
        "name": "LABEL",
        "type": "string",
        "categorical": true
      },
      {
        "name": "CAST_ID",
        "type": "string",
        "categorical": true
      },
      {
        "name": "DURATION",
        "type": "long"
      },
      {
        "name": "PUBLISH_AT",
        "type": "long"
      }
    ],
    "version": "1.0"
  }
when "interactions"
  {
    "type": "record",
    "name": "Interactions",
    "namespace": "com.amazonaws.personalize.schema",
    "fields": [
      {
        "name": "USER_ID",
        "type": "string"
      },
      {
        "name": "ITEM_ID",
        "type": "string"
      },
      {
        "name": "EVENT_TYPE",
        "type": "string"
      },
      {
        "name": "EVENT_VALUE",
        "type": "float"
      },
      {
        "name": "TIMESTAMP",
        "type": "long"
      }
    ],
    "version": "1.0"
  }
end

schema_arn = personalize.create_schema(
  name: SCHEMA_NAME,
  schema: avro_schema.to_json
).schema_arn

データセット

DATASET_NAME = "users"
DATASET_TYPE = "users" # or "items" or "interactions "
SCHEMA_ARN = "arn:aws:personalize:{{REGION}}:{{ACCOUNT_ID}}:schema/{{SCHEMA_NAME}}"
DATASET_GROUP_ARN = "arn:aws:personalize:{{REGION}}:{{ACCOUNT_ID}}:dataset/{{DATASET_GROUP_NAME}}"

personalize = Aws::Personalize::Client.new

personalize.create_dataset(
  name: DATASET_NAME,
  schema_arn: SCHEMA_ARN,
  dataset_group_arn: DATASET_GROUP_ARN ,
  dataset_type: DATASET_TYPE 
)

学習用データを置くS3バケット

Amazon Personalize からアクセスできるようにバケットポリシーを設定する必要があります。

BUCKET_NAME = "myapp-personalize"

s3 = Aws::S3::Client.new

s3.create_bucket(bucket: S3_BUCKET_NAME)
s3.put_bucket_policy(
  bucket: S3_BUCKET_NAME,
  policy: {
    "Version": "2008-10-17",
    "Id": "PolicyForPersonalizePrivateContent",
    "Statement": [
      {
        "Sid": "PersonalizeS3BucketAccessPolicy",
        "Effect": "Allow",
          "Principal": {
            "Service": "personalize.amazonaws.com"
        },
        "Action": [
          "s3:GetObject",
          "s3:ListBucket"
        ],
        "Resource": [
          "arn:aws:s3:::#{S3_BUCKET_NAME}",
          "arn:aws:s3:::#{S3_BUCKET_NAME}/*"
        ]
      }
    ]
  }.to_json
)

学習用データのインポートに使う実行ロール

AmazonPersonalizeFullAccess のサービスロールを使うのに加え、学習用データを置くS3バケットへのアクセスを許可します。

BUCKET_NAME = "myapp-personalize"
ROLE_NAME = "personalizeExecutionRole"
ROLE_PATH = "/myapp/"

iam = Aws::IAM::Client.new

role = iam.create_role(
  assume_role_policy_document: {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "personalize.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      }
    ]
  }.to_json,
  path: ROLE_PATH,
  role_name: ROLE_NAME,
).role
iam.attach_role_policy(
  policy_arn: "arn:aws:iam::aws:policy/service-role/AmazonPersonalizeFullAccess",
  role_name: role.role_name,
)
iam.put_role_policy(
  policy_document: {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": [
          "s3:ListBucket"
        ],
        "Effect": "Allow",
        "Resource": [
          "arn:aws:s3:::#{S3_BUCKET_NAME}"
        ]
      },
      {
        "Action": [
          "s3:GetObject",
          "s3:PutObject"
        ],
        "Effect": "Allow",
        "Resource": [
          "arn:aws:s3:::#{S3_BUCKET_NAME}/*"
        ]
      }
    ]
  }.to_json,
  policy_name: "AllowAccessToS3Bucket",
  role_name: role.role_name,
)

イベントトラッカー

イベントトラッキングを行うには、イベントトラッカーを作る必要があります。 イベントトラッカーを作ると、イベントトラッカーが受け取ったデータを投入するデータセットなども一緒に作られます。

docs.aws.amazon.com

DATASET_GROUP_ARN = "arn:aws:personalize:{{REGION}}:{{ACCOUNT_ID}}:dataset/{{DATASET_GROUP_NAME}}"

personalize = Aws::Personalize::Client.new

event_tracker_arn = personalize.create_event_tracker(
  name: EVENT_TRACKER_NAME,
  dataset_group_arn: DATASET_GROUP_ARN
).event_tracker_arn

学習用データのインポート

ユーザー、アイテム、インタラクションそれぞれ学習させたいデータセットタイプにあわせてCSVを作成します。 アップロードしたCSVを指定したデータセットインポートジョブを作成します。

データセットインポートジョブのパラメータとして、インポート先のデータセット、データセットタイプに対応するスキーマで作成したCSVのS3オブジェクトキー、実行IAMロールを指定します。

# CSV
BUCKET_NAME = "myapp-personalize"
OBJECT_KEY = "/users.csv"
FILE_PATH = "/tmp/users.csv"
# Amazon Personalize リソース
DATASET_ARN = "arn:aws:personalize:{{REGION}}:{{ACCOUNT_ID}}:dataset/{{DATASET_GROUP_NAME}}/{{DATASET_NAME}}" # 作成したデータセット
ROLE_ARN = "arn:aws:iam::{{ACCOUNT_ID}}:role/myapp/{{ROLE_NAME}}" # 作成した実行ロール

s3 = Aws::S3::Client.new

File.open(FILE_PATH, 'r') do |file|
  s3.put_object(
    bucket: BUCKET_NAME,
    key: OBJECT_KEY,
    body: file,
    content_type: 'text/csv'
  )
end

personalize = Aws::Personalize::Client.new

personalize.create_dataset_import_job(
  job_name: "users-import-job-#{Time.current.to_i}",  # 重複はできない
  dataset_arn: DATASET_ARN,
  data_source: {
    data_location: File.join('s3://', BUCKET_NAME , OBJECT_KEY)
  },
  role_arn: ROLE_ARN,
)

リアルタイムイベントトラッキング

EVENT_TRACKER_TRACKING_ID = "0000000-0000-0000-0000-000000000000"
USER_ID = current_user.id
SESSION_ID = session.id
EVENT_TYPE = "rating"  # 今回は★1-5のレビュー評価をトラッキングする想定。自由に決められる。
ITEM_ID = review.item_id
EVENT_VALUE = review.rating
SENT_AT = review.created_at

personalize_events = Aws::PersonalizeEvents::Client.new

payload = {
  tracking_id: EVENT_TRACKER_TRACKING_ID,
  user_id: USER_ID.to_s,
  session_id: SESSION_ID ,
  event_list: [
    {
      event_type: EVENT_TYPE,
      properties: {
        itemId: ITEM_ID.to_s,
        eventValue: EVENT_VALUE,
      }.to_json,
      sent_at: SENT_AT,
     }
  ]
}
personalize_events.put_events(payload)

レコメンデーション結果の取得

アイテムベース (SIMS)も、ユーザーベースも、レコメンデーション結果を取得するAPIは同じです。 パラメータが違います。

アイテムベース (SIMSレシピのソリューションを使ったキャンペーン)

item_id を渡します。

CAMPAIGN_ARN = "arn:aws:personalize:{{REGION}}:{{AWS_ACCOUNT_ID}}:campaign/{{CAMPAIGN_NAME}}"
ITEM_ID = 1234
NUM_RESULTS = 20

personalize_runtime = Aws::PersonalizeRuntime::Client.new

# item_list[0].item_id #=> String
# item_list[0].score #=> 常に 0
item_list = personalize_runtime.get_recommendations(
  campaign_arn: CAMPAIGN_ARN ,
  item_id: ITEM_ID.to_s,
  num_results: NUM_RESULTS,
).item_list

ユーザーベース (HRNN レシピのソリューションを使ったキャンペーン)

user_id を渡します。

結果セットにスコアが含まれるので、おすすめ順に並べるのに使えますね。

CAMPAIGN_ARN = "arn:aws:personalize:{{REGION}}:{{AWS_ACCOUNT_ID}}:campaign/{{CAMPAIGN_NAME}}"
USER_ID = 1234
NUM_RESULTS = 20

personalize_runtime = Aws::PersonalizeRuntime::Client.new

# item_list[0].item_id #=> String
# item_list[0].score #=> Float
item_list = personalize_runtime.get_recommendations(
  campaign_arn: CAMPAIGN_ARN ,
  user_id: USER_ID.to_s,
  num_results: NUM_RESULTS,
).item_list