Laravel + AWS Personalize で「お客さんコレ好きでしょ?」機能をつくる

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、現在AWSといえばAmazonが提供するクラウドサービスとして有名な存在になりました。

私もずっと注目していたのですが、あいにくAWSをガッツリ使うという開発に携わることがなかったため、正直なところ「出遅れた」感があり、そのまま放置してきました。

しかし、さすがにそれでは今後のためにならないだろうと思い、少しずつ体験することにしました。

そして、今回その一発目として、以前クライアントさんから教えてもらったAWS Personalizeで、

『お客さんコレ好きでしょ?』

機能をつくっていたいと思います。

これは、いわゆる「レコメンデーション(おすすめ)機能」のことで、例えばAmazonでお買い物をしているときに、「こんな商品もありますけどね…?(チラッ😘)」というようなコンテンツが出ることがあると思います。

まさにこれがレコメンデーション機能ですね。

ということで、今回はLaravelを使って実装してみたいと思います。

「ジークンドーの動きって
スゴイですね👊✨」

開発環境: Laravel 8.x、Analytics Reporting API v4、AWS Personalize

Amazon Personalize でレコメンデーション機能を実装する流れ

Amazon Personalizeは機械学習を使って「このお客さんは、きっとこの商品が好き!」を見つけ出すことができます。

流れとしておおまかに次のようになります。

  1. データを用意する
  2. AWSへアップロード
  3. 機械学習する
  4. APIから「おすすめデータ」を取得する

では実際にやってみましょう❗

準備する

今回は学習データとして、今見ていただいてるブログ「Console dot Log」のアクセスログを使います。

そこで、Google Analyticsが提供するAnalytics Reporting APIを使えるようにしていきます。

パッケージをインストールする

以下のコマンドを実行してください。

composer require google/apiclient:^2.0

また、AWSの認証設定は後でご紹介しますが、同じくcomposerを使うので先にこちらもインストールしておいてください。

composer require aws/aws-sdk-php

認証用JSONファイルをつくる

そして、Google Cloud Platform へログインし、APIアクセスの際に必要になる認証ファイル(JSON)を用意しておいてください。

📝 参考URL: https://blog.capilano-fw.com/?p=1816#i-2 

なお、JSONファイルを作成したらサービスアカウントのメールアドレスが取得できます。後で使うので控えておいてください。(JSONファイル内にも同じメールアドレスが書かれています)

Analytics Reporting APIの準備をする

では次に、Analytics APIを有効にします。
Google Cloud Platform画面左にある「APIとサービス」をクリック。

そして、「API とサービスの有効化」リンクをクリック。

検索ボックスが表示されるので、「analytics」と入力。

検索結果の中から「Google Analytics Reporting API」を探してクリック。

最後に「有効にする」ボタンをクリックしたら完了です。

Google Analyticsに権限をセットする

次に、Google Analyticsで先ほど作成したサービスアカウント(JSONファイルをつくったときのメールアドレス)を登録してアクセス権限を与えます。

まず、Google Analytics(⚠ご注意:Google Cloud Platformではないです)へログインし、ページ右下にある「管理」リンクをクリック。

アカウントユーザーの管理をクリック。

」ボタンから「ユーザーを追加」リンクをクリック。

すると、メールアドレスの入力ボックスが表示されるので、ここにJSONファイルを作成したときに取得した「********@iam.gsserviceaccount.com」を入力します。(つまり、Google CloudのアカウントがGoogle Analyticsにアクセスできるようにしています)

※ なお、メール通知は不要なのでチェックは外しました。

そして、最後に「追加」ボタンをクリックしたらGoogle Analyticsでの作業は完了です👍

学習用データを作る

Analytics Reporting APIの用意ができたので、続いて機械学習に使うデータを用意していきます。

AWS Personalizeで使うデータの概要

AWS Personalizeでは、以下3つのデータを使って機械学習をすることができます。

  • interactions: どのユーザーはどの商品に好きか?(例:ある商品にアクセスした、いいねを押した等)
  • users: 年齢、性別、プレミアム会員かどうか、などのユーザー情報
  • items: 価格などの商品情報

※ ただし、必須なのはinteractionsだけなので、今回はusersitemsは省略します。

なお、学習データとしてGoogle Analyticsから取得するデータは次のとおりです。

  • USER_ID: ユーザーの居住地(Tokyo、Osakaなど)※1
  • ITEM_ID: ブログ記事のページ番号
  • TIMESTAMP: アクセス日時

※1:Analytics APIではユーザーを特定するようなIDを発見できませんでしたのでga:regionを使うようにしました。(もしあれば教えてください…plz)

つまり、このデータは以下のようになります。

  • 東京に住んでいる人は ページ 111 の記事が好き
  • 大阪に住んでいる人は ページ 222 の記事が好き
  • 福岡に住んでいる人は ページ 333 の記事が好き

そして、実運用ではIPアドレスなどから住んでいる地方を割り出し「これ好きでしょ❓」コンテンツとして提供することになります。

では、実際にLaravelで作業をしていきましょう❗

コントローラーをつくる

まずは、コントローラーです。
以下のコマンドを実行してください。

php artisan make:controller GoogleAnalyticsController

すると、ファイルが作成されるので中身を以下のように変更してください。

app/Http/Controllers/GoogleAnalyticsController.php

<?php

namespace App\Http\Controllers;

use Carbon\Carbon;
use Illuminate\Http\Request;

class GoogleAnalyticsController extends Controller
{
    private $analytics, $csv_file;

    public function __construct() {

        $api_key_path = storage_path('app/json/google_api_key.json');
        $client = new \Google_Client();
        $client->setAuthConfig($api_key_path);
        $client->setScopes([\Google_Service_Analytics::ANALYTICS_READONLY]);
        $this->analytics = new \Google_Service_AnalyticsReporting($client);

        $csv_path = storage_path('app/csv/aws_personalize.csv');
        file_put_contents($csv_path, '');
        $this->csv_file = new \SplFileObject($csv_path, 'w');
        $this->csv_file->fputcsv(['USER_ID', 'ITEM_ID', 'TIMESTAMP']);

    }

    public function make_aws_data() {

        $dt = today();

        for($i = 0 ; $i < 10 ; $i++) {

            $end_dt = $dt->copy(); // 月の終了日
            $dt->subDays(10);   // 10日ずつさかのぼる
            $start_dt = $dt->copy(); // 月の開始日
            $dt->subDay();   // 同じ日が重複しないように1日ずらす

            $reports = $this->getAnalyticsReport($start_dt, $end_dt); // 該当期間のレポートを取得(最大10,000件)

            foreach ($reports as $report) {

                $header = $report->getColumnHeader();
                $dimensionHeaders = $header->getDimensions();
                $rows = $report->getData()->getRows();

                foreach ($rows as $row) {

                    $dimensions = $row->getDimensions();
                    $user_id = ''; // 今回はユーザーの地方(Tokyo, Osakaなど)
                    $item_id = ''; // ブログ記事のページ番号
                    $timestamp = -1; // UNIXタイムスタンプ

                    foreach ($dimensions as $index => $dimension) {

                        $dimension_header = $dimensionHeaders[$index];

                        if($dimension_header === 'ga:pagePath') {

                            $pattern = '|p=([0-9]+)|';

                            if(preg_match($pattern, $dimension, $matches)) {

                                $item_id = $matches[1];

                            }

                        } else if($dimension_header === 'ga:dateHourMinute') {

                            $timestamp = Carbon::createFromFormat('YmdHi', $dimension)->timestamp;

                        } else if($dimension_header === 'ga:region') {

                            $user_id = $dimension;

                        }

                    }

                    if(!empty($user_id) && !empty($item_id) && $timestamp > 0) {

                        $this->csv_file->fputcsv([
                            $user_id,
                            $item_id,
                            $timestamp
                        ]);

                    }

                }

            }

        }

    }

    private function getAnalyticsReport($start_dt, $end_dt) {

        $analytics_view_id = env('ANALYTICS_VIEW_ID');

        // Metrics
        $dateRange = new \Google_Service_AnalyticsReporting_DateRange();
        $dateRange->setStartDate($start_dt->format('Y-m-d'));
        $dateRange->setEndDate($end_dt->format('Y-m-d'));

        $page_views_metric = new \Google_Service_AnalyticsReporting_Metric();
        $page_views_metric->setExpression("ga:pageviews");
        $page_views_metric->setAlias("pageviews");

        $request = new \Google_Service_AnalyticsReporting_ReportRequest();
        $request->setViewId($analytics_view_id);
        $request->setDateRanges($dateRange);
        $request->setMetrics([$page_views_metric]);

        // Dimensions
        $page_path_dimension = new \Google_Service_AnalyticsReporting_Dimension();
        $page_path_dimension->setName('ga:pagePath');

        $date_dimension = new \Google_Service_AnalyticsReporting_Dimension();
        $date_dimension->setName('ga:dateHourMinute');

        $region_dimension = new \Google_Service_AnalyticsReporting_Dimension();
        $region_dimension->setName('ga:region');

        $request->setDimensions([
            $page_path_dimension,
            $date_dimension,
            $region_dimension
        ]);
        $request->setFiltersExpression('ga:pagePath=~p=[0-9]+$'); // p=数字がついてるもので絞り込み
        $request->setPageSize(10000);

        $order_by = new \Google_Service_AnalyticsReporting_OrderBy();
        $order_by->setFieldName('ga:dateHourMinute');
        $order_by->setOrderType('VALUE');
        $order_by->setSortOrder('ASCENDING');

        $request->setOrderBys($order_by); // データが偏らないよう日付による並べ替えをセット

        $body = new \Google_Service_AnalyticsReporting_GetReportsRequest();
        $body->setReportRequests([$request]);

        return $this->analytics->reports->batchGet($body);

    }
}

この中では過去100日分(10日ずつ時間をさかのぼる × 10回)でAnalytics Reporting APIからデータを取得し、さらにデータをAWS用に形を変えてCSVとして保存しています。

なお、データ取得する$analytics_view_idGoogle Analytics内で、以下のようにして取得できますので、.envに書き込んでおいてください。

.env

ANALYTICS_VIEW_ID=********

ルートをつくる

では、続いてルートです。

routes/web.php

use App\Http\Controllers\GoogleAnalyticsController;

// 省略

Route::prefix('google_analytics')->group(function(){

    Route::get('make_aws_data', [GoogleAnalyticsController::class, 'make_aws_data']);

});

データをつくる

では、準備が完了しましたので、実際に「http://******/google_analytics/make_aws_data」へアクセスして学習データを作成してみましょう❗

すると・・・・・・

このようにCSVファイルが作成されました。

AWSで機械学習をする

では、本題のAWS Personalizeです。

S3へCSVファイルをアップロードする

先ほど作成したCSVファイルはS3(AWSのストレージサービス)経由で提供することになりますので、まずはアップロードから行いましょう。

なお、バケットの作り方は以下のページを参考にしてみてください。

📝 参考URL: バケット(保存領域)をつくる

該当するバケットを選択してページ移動し、「アップロード」ボタンをクリックします。

すると、ファイル選択ダイアログが表示されるので、先ほど作成した「aws_personalize.csv」を選択し、ページ下部にある「アップロード」ボタンをクリックしてください。

アップロードが完了すると、aws_personalize.csvが一覧表示されるのでファイル名をクリックします。

すると詳細が表示されるので、「S3 URI」に書かれているテキストを控えておきます。(後で、AWS Personalize の方で指定します)

また、今の状態ではこの後で使うAWS Personalizeからのアクセス権限がついていません。そのため、次の手順でアクセス権限をつけてあげます。

まず「アクセス許可」リンクをクリック。

移動先のページの中にある「バケットポリシー」の「編集する」ボタンをクリック。

ポリシーの中へ以下のJSONコードを入力し、保存してください。

{
    "Version": "2012-10-17",
    "Id": "PersonalizeS3BucketAccessPolicy",
    "Statement": [
        {
            "Sid": "PersonalizeS3BucketAccessPolicy",
            "Effect": "Allow",
            "Principal": {
                "Service": "personalize.amazonaws.com"
            },
            "Action": [
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::(ここをあなたのバケット名に入れかえる)",
                "arn:aws:s3:::(ここをあなたのバケット名に入れかえる)/*"
            ]
        }
    ]
}

入力したところの例はこちら。

なお、この部分については以下のページがわかりやすいです。

📝 参考URL: https://aws.amazon.com/jp/premiumsupport/knowledge-center/personalize-import-insufficient-privileges-s3-error/

AWS Personalizeで機械学習をする

インポートする

では、続いて機械学習の部分です。
先ほどと同じくページ上部から「personalize」と検索して移動します。

そして、ページ移動したら「Get started」ボタンをクリック。

データセットのグループ名を入力して「Next」ボタンをクリック。

詳細を入力するページが表示されるので、データセット名に「region」、そして、Schemaは新しく「basic-schema」という名前のものをつくります。

では、これで「Next」ボタンをクリックします。

そして、次にデータセットのインポート情報の入力です。
まずroleを作成しますので、「Create role」を選択。

すると、以下のようなポップアップが表示されるので、今回は全てのバケットへの権限を与えることにします。

Any S3 bucket」を選択して「Create role」をクリックしてください。

これでroleが作成されたので、後は以下のように「インポート作業名」、そして、先ほどS3で取得したURLを入力。

そして最後に「Finish」ボタンをクリックします。

すると、以下のように「作業中ですよ👍」という表示が現れますので、しばらくヒカルくんやエガちゃんねるでも見て時間をつぶしてください😂

そして・・・・・・・

時間が経って、Activeという表示になったらインポートは完了です。

学習データ(solution)をつくる

では、インポートしたデータを使って学習済みデータ「Solution」をつくっていきましょう。

同じページにある「Solution」の項目に「Start」ボタンが表示されていると思いますので、これをクリックします。

すると、学習方法を選択するページが表示されるので、適当な名前を入力し、「aws-user-personalization」を選択します。

選択が終わったら、「Next」ボタンをクリック。

確認ページが表示されるので、「Finish」ボタンをクリックしてください。

するとまた、「作業中ですよ👍」状態になるので、朝倉未来さんやけいちょんチャンネルでも見ながら時間を待ってください。(私の場合、30分ぐらいかかりました)

完了したら、次はCampaignです。

APIが使えるようにする(Campaign)

機械学習データを作ってもそのままでは、APIが用意されていません。

そのため、次はLaravelからAWS Personalizeにアクセスできるよう専用APIを作っていきましょう。

先ほどのSolutionのすぐ右側にCampaignの項目があるので、「Create campaign」ボタンをクリックします。

すると、詳細ページが表示されるので、キャンペーン名を入力、さらに先ほど作成したSolutionを選択します。

そして、ページ下部にある「Create campaign」ボタンをクリックします。

すると、やはり「作業中ですよ👍」になりますので、少し待ちます(今回は表示が違うので分かりにくいかもしれませんが、比較的すぐ終わりますのでYouTubeは見ないほうがいいです😂)

そして・・・・・・・

キャンペーンの作成が完了すると、すぐ下にテストできるセクションがでるので、Tokyoと入力して東京に住んでる人たちの「これ好きでしょ❓」データを取得してみましょう。

すると・・・・・・

はい❗

すぐ下にRecommendationセクションが表示されました。
どうやらScoreは「よりおすすめ」指数のようですね。

これで機械学習部分は完了です😊

なお、同じページにARN(Amazon リソースネーム)が書かれています。
次の項目で必要になるので、この文字列を控えておいてください。

LaravelからAWS Personalizeにアクセスしておすすめデータ取得する

認証情報を取得する

LaravelからAWSのAPIに接続するには、認証情報が必要になります。

そのため、先にAWSから「アクセスキーID」と「シークレットアクセスキー」を取得して.envへ設置しておいてくだい。

📝 参考URL: アクセスキー&秘密キーを取得する

.env

AWS_ACCESS_KEY_ID="(ここにアクセスキーID)"
AWS_SECRET_ACCESS_KEY="(ここにシークレットアクセスキー)"
AWS_DEFAULT_REGION=ap-northeast-1

※ これらのキーはすでに.envに存在しています。

また、先ほどCampaignを作成したときに取得したARNもここに書き込んでおきましょう。

AWS_RECOMMENDATION_ARN="arn:aws:personalize:ap-northeast-1:***********************:"

コントローラーをつくる

では、コントローラーです。
以下のコマンドを実行してください。

php artisan make:controller AwsPersonalizeController

するとファイルが作成されるので、中身を以下のように変更します。

app/Http/Controllers/GoogleAnalyticsController.php

<?php

namespace App\Http\Controllers;

use Aws\PersonalizeRuntime\PersonalizeRuntimeClient;
use Illuminate\Http\Request;

class AwsPersonalizeController extends Controller
{
    public function get_recommendations() {

        $user_id = 'Tokyo'; // 👈 Tokyoに住んでいる人へのおすすめを取得します
        $client = new PersonalizeRuntimeClient([
            'region' => 'ap-northeast-1',
            'version' => 'latest'
        ]);
        $result = $client->getRecommendations([
            'campaignArn' => env('AWS_RECOMMENDATION_ARN'),
            'userId' => $user_id,
        ]);

        $recommendations = $result->get('itemList');

        foreach($recommendations as $recommendation) {

            $item_id = $recommendation['itemId'];
            $score = $recommendation['score'];
            $url = 'https://blog.capilano-fw.com/?p='. $item_id;

            echo '<a href="'. $url .'" target="_blank">'. $url .'</a><br>Score: '. $score .'<hr>';

        }

    }
}

この中では、「Tokyoに住んでいるへのおすすめ」取得しています。

そのため、実際の運用では ipinfo.io などのサービスでアクセスした人がどの地方にいるかを取得し、それを$user_idとしておすすめ情報を取得することになります。

ルートを追加する

続いてルートです。

routes/web.php

use \App\Http\Controllers\AwsPersonalizeController;

// 省略

Route::prefix('aws_personalize')->group(function(){

    Route::get('get_recommendations', [AwsPersonalizeController::class, 'get_recommendations']);

});

これで全てが完了しました❗

テストしてみる

では、実際にブラウザでアクセスしてみましょう❗

ブラウザで「http://*****/aws_personalize/get_recommendations」にアクセスします。

すると・・・・・・

スコア付きでリンクが表示されました❗
では、次にTokyoKanagawaに変えてリロードしてみましょう。

すると・・・・・・

はい❗
内容の違うおすすめページが表示されました。

成功です😊(精度としても「こんな感じだろうな」というラインナップになっていました👍)

企業様へのご提案

今回のAWS Personalizeで何ができるかを考えてみました。
ぜひ気になるものがありましたら、お気軽にご相談ください👍

  • 過去の売上データの中から年齢・性別・家族構成などを取り出し「どの商品を提案すれば気にいってもらいやすいか」を割り出す
  • 時間帯で区切った売上データを使って「この時間帯は●●が売れやすいか」のデータを取得する
  • ユーザーの居住地(都道府県や市区町村)データを使ってよく閲覧されているページを調べる
  • 過去の天気や温度データを使って、「晴れの日はこれが売れる」「寒い日は意外とことが人気になる」などを見つけ出す
  • マッチングサービスで、「こういう経歴の人はこういう人とマッチングしやすい」など

参考にしたページ

今回の開発をするに当たって以下3つのページを参考にさせていただきました。特にDevelopersIOさんの方はスクリーンショットまで用意してくれていたのでとても助かりました!

Amazon Personalize – Real-Time Personalization and Recommendation for Everyone(本家のブログ)

AWS Personalize : 開始方法(これも本家)

Amazon Personalizeを使ってみた(DevelopersIO)

先人たちに感謝❗
有益な情報、ありがとうございます。m(_ _)m

ちなみに:今回のかかった料金

ちなみに今回は(1回目間違えたので)合計2回機械学習をしました。
そして、その料金は次のとおりでした。

実現できることと比べるとホント格安ですよね👍

おわりに

ということで、今回はAWS Personalizeを使って「これ好きでしょ❓」機能を作ってみました。

AWSEC2S3ばかりで他のものをあまり使ったことがありませんでしたが、今回念願の機械学習を使うことができました。

なお、雑感としてはこういった機械学習・AIは大量のデータが必要になってくるので、やはり規模の大きい企業が有利だろうなという気がしました。

というのも、規模が小さいとデータそのものの量も多くなりにくいですし、たくさんの条件下のデータではないので偏り含まれる可能性が高いからです。

そう考えると、やはり日頃から何かとデータは残しておくべきということになるので、これも先日別のクライアントさんから教えていただいた Obniz のようなキットを使ってデータを「貯金」していくことが今後の戦略としては正しいのかもしれません。

ぜひ皆さんも「これ好きでしょ?」機能を試してみてくださいね。

ではでは〜❗

「予想以上に記事のボリュームが
大きくなってしまいました💦」

開発のご依頼お待ちしております 😊✨ お問い合わせ
また、こちらもお待ちしております。
  • 実案件の開発サポート: 詳細
  • ツイッターのフォロー: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly