イノベーション エンジニアブログ


株式会社イノベーションのエンジニアたちの技術系ブログです。ITトレンド・List Finderの開発をベースに、業務外での技術研究などもブログとして発信していってます!


このエントリーをはてなブックマークに追加

Pythonで購入履歴を使ったレコメンドを作成してみた② 〜AWS Lambda編〜

どうも、中村です。

前回に作成したプログラムだけでは実際に利用するのが難しいので、AWS Lambda + API Gatewayを使ってAPI化していきたいと思います。

※ちょっと長くなりそうだったので、API Gateway編は次回書きます。

前回作成した構成

├── data.py ← 履歴データ
└── get_recommend.py ← レコメンド取得プログラム

こちらをAWS Lambdaに設定していくのですが、デプロイするためにファイルを圧縮したものを作成しましょう。

上記ディレクトリへ移動した後に以下コマンドを実行すれば、全てのファイルを圧縮したものが作成されます。

$ zip -r get_recommend.zip *

実行後

├── data.py
├── get_recommend.py
└── get_recommend.zip

get_recommend.zip というファイルをLambda関数の作成時に使用していきます。

AWS Lambda関数の作成

では早速Lambda関数を作成していきましょう。

01.png 関数の作成をクリック。

02.png 「一から作成」を選びます。(デフォルトで選択済み)

03.png 名前は分かりやすいものでOK。
ランタイムには前回Python3系で作成したので、「Python 3.6」を選択します。

04.png 今回新規のロールを作成するので、「テンプレートから新しいロールを作成」を選択。
ロール名も分かりやすいものであれば適当でOKです。

05.png Lambda関数が無事作成されました。

コードをアップロードする

それでは前回作成したコードをアップロードしていきます。

06.png 関数コードというパネルのコードエントリタイプから「.ZIPファイルをアップロード」を選択し、先ほど作成した「get_recommend.zip」を選択します。

07.png 右上の「保存」ボタンをクリックするとアップロードされた内容が表示され、コンソール上で編集が可能になります。

08.png

コードを修正する

前回作成したプログラムはコンソール上から実行させるものでしたので、修正する必要があります。

大まかに説明すると、以下の2点になります。

・ コマンドライン引数は使用しない
・ メイン部分をハンドラーへ変更

それぞれ修正していきましょう。 09.png

10.png メイン部分も変更していきます。
コマンドライン引数にて渡していた「対象製品ID」は、handlerのeventから取得するようにします。

11.png 最後に関数コードパネルの上部にあるハンドラ部分を、今回のプログラムに合うように変更します。

get_recommend.pyのlambda_handlerを実行させたいので、

get_recommend.lambda_handler

とします。

テストイベントの設定

修正したコードをデバッグしていきたいので、テストイベントを設定していきましょう。

12.png 画面上部にある「テスト」ボタンをクリックします。

13.png イベントテンプレートは「Hello World」でOK。(デフォルトで選択済み)

イベント名は何でも良いです。
(個人的には渡す値などが判別できる名称にしたかったのですが、英数字のみに限定されているため上手く表現できませんでした)

最下部の編集エリアには、テスト時に渡したい値などを記入しておきます。

上記のように記述しておくと、Lambda関数側で以下のように値を渡すことができます。

def lambda_handler(event, context):
    print(event.product_ids) ←"111,222"

14.png 作成したテストイベントが選択されているはずなので、テストボタンをクリックしてみましょう。

。。。エラーがでますね(笑)

ライブラリをアップロードする

前回プログラムを作成した際には、pipにてライブラリをインストールしました。が、Lambdaではpipを使ってインストールすることは出来ないため以下の手順でライブラリをアップロードしていきます。

1. ローカル環境のプロジェクトディレクトリに保存するようにライブラリをインストールし直す

2. 再度全てのファイルを圧縮

3. Lambdaにてファイルをアップロード

今回エラーが発生したライブラリは「nltk」というライブラリだったので、以下のようなコマンドになります。

$ pip install nltk -t .  ←現在のディレクトリにパスを指定して保存
$ zip -r get_recommend.zip * ←全ファイルを圧縮

再度AWSのコンソールからZIPファイルをアップロードしてみましょう。

15.png ライブラリがアップロードされました。これで実行できるはずです。

。。。できませんでした(汗)

_sqlite3なんて使ってないけど?

16.png ログとして出力しているものの、先ほどとは違い心当たりがない。。。

とりあえず調べてみると、以下のサイトを見つけました。 https://stackoverflow.com/questions/44058239/sqlite3-error-on-aws-lambda-with-python-3

なるほど、例のnltkライブラリの依存関係でエラーが発生しているようです。
またこちらの回答にあるように、sqlite3を使っていない場合の対応を行えば上手くいけそうです。

17.png

結果

18.png ようやく完成しました!

最終的なコードはこんな感じ。

import collections
import imp
import sys
sys.modules["sqlite"] = imp.new_module("sqlite")
sys.modules["sqlite3.dbapi2"] = imp.new_module("sqlite.dbapi2")
from nltk.metrics import jaccard_distance
from data import data_set

def get_similar_user(product_ids, cv_data):
	"""
	類似度が高いユーザーを、指定した製品idから抽出する

	Parameters
	----------
	product_ids : list
		指定した製品id
	cv_data : dict
		CVしたユーザーデータ

	Returns
	-------
	target_users : list
		類似度が高い順にソートされたユーザー情報
	"""
	target_users = {};

	# CVデータ分類似度を検証させる
	for user in cv_data:
		# 類似度を算出
		jaccard = jaccard_distance(set(product_ids), set(cv_data[user]))

		# 全く同じデータは省く
		if jaccard != 1.0:
			target_users[jaccard] = user

	# 類似度が高い順にソートして返す
	return sorted(target_users.items())


def get_target_product_ids(product_ids, target_user, cv_data):
	"""
	類似度が高いユーザーを対象に、非選択の製品idを抽出する

	Parameters
	----------
	product_ids : list
		指定した製品id
	target_user : list
		類似度の高いユーザー
	cv_data : dict
		CVしたユーザーデータ

	Returns
	-------
	target_product_ids : list
		非選択の製品id
	"""
	target_product_ids = []

	# 類似度が高いユーザー分処理
	for jaccard, user in target_user:
		# 指定した製品id以外の製品idを取得
		diff = list(set(cv_data[user]) - set(product_ids))

		# 取得した製品idをリストに追加
		target_product_ids.extend(diff)

	return target_product_ids


def lambda_handler(event, context):
	# イベントから渡したデータをセット
	product_ids = [int(id.strip()) for id in event["product_ids"].split(',')]

	# 類似度が高いユーザーを、指定した製品idから抽出する
	target_users = get_similar_user(product_ids, data_set)

	# 類似度が高いユーザーを対象に、非選択の製品idを抽出する
	target_product_ids = get_target_product_ids(product_ids, target_users, data_set)

	# 選択数を算出
	counter = collections.Counter(target_product_ids).most_common()

	# 製品idのみをカンマ区切りの文字列として返すなら以下のようにする
	return (','.join([str(id) for id, count in counter]))

感想

前回作成したプログラムをそのままLambda関数として使用することはできず、結構修正が必要でした。

最終的にAPI化するのであれば、最初からLambda関数として作成した方が断然早いなーと思いました。

次回はAPI Gatewayの設定をする予定です!
こちらからは以上です!