アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

レコメンド - アイテムベース協調フィルタリング -

こんにちは江口です。

今回は、先日社内勉強会で以下の内容について発表しましたので、ブログでも共有させて頂きます。

【概要】
・アイテムベース協調フィルタリング(商品レコメンド)

- アイテムベース協調フィルタリングを実装してみた
- ユーザーが購入した商品の中からjaccard指数で類似商品を見つけ出す
- 対象商品をベースに同じ商品を購入している別のユーザーが購入した別の商品をレコメンド
- あとdjango使ってみた


▼技術要件
python 3.6.1
・django1.9
・heroku
・DB・・・heroku上はpostgress、localはsqlite3


▼レコメンドアルゴリズムについて

今回試してみたのは、「アイテムベース協調フィルタリング」と呼ばれる、商品が主語となる簡易的なレコメンドアルゴリズムになります。

よく見る「この商品を買った人はこんな商品も〜」みたいにオススメしてくるあれです。

アイテムベース協調フィルタリングの一環、「jaccard指数」による類似度算出を実際にコード組んで検証してみました。

▼jaccard指数とは。。。

実際に勉強会で使った資料を貼り付けちゃいます。。。。



こんな感じのアルゴリズムです。
例えば、Aさんが買った商品は「商品1」と「商品3」になります。
そしてAさんが買った「商品1」とその他全ての商品がどれだけ商品1と類似しているか?


『A&&B / A||B => Jaccard指数』でスコアリングしていきます。
資料の例だと、商品3がもっとも類似してますね。

ただ、Aさんはすでに商品3を購入してますので、除外して、商品4をレコメンドすることもできますね。

▼実際に触って見れます!
上で説明したアルゴリズムを実際に検証できるサンプルシステムを作ってみました。
・画面
https://lit-headland-28550.herokuapp.com/recommend/
(無料枠herokuなので、30分アクセスがないと立ち上がりに時間がかかることがあります。。ご了承ください。)

TOPページではこんな画面が表示されるかと思います。


ちょっとわかりにくいのですが、擬似的なECサイトを模してます。
「購入者」プルダウンで仮想敵にログインユーザーになったとして、「商品一覧」にある商品の「購入」ボタンを押すことで選択したユーザーで買い物ができます。

買い物が済むと、こんな感じで、購入した商品に類似したレコメンド商品一覧と、ユーザーの買い物履歴が確認できます!



自由にユーザーになりすまし、商品を買ったりキャンセルしたりしてみて、レコメンド具合を確かめて見てください。
私が試した時は、「PHP参考書」購入したら、「Java参考書」と「タンクトップ」がなぜかレコメンドされました。。。


ソースコード

ソースコードgithubで公開してます。
https://github.com/yueguchi/recommend-with-django

要点のみ、紹介させて頂きます。

・商品一覧の取得

商品一覧の取得は、requestをrecommend/view.pyにてrequestw受け取っています。
https://github.com/yueguchi/recommend-with-django/blob/master/recommend/views.py

この中の



def index(request):
    """
    商品一覧の閲覧・購入から、レコメンド(jaccardベース)を表示する
    """
    # 商品一覧
    items = Items.objects.all()
    # 購入者一覧(値だけのlistを取得する)
    users = Users.objects.all()#.values_list('name', flat=True)

    # レコメンド取得
    user_name = request.GET.get('user_name', '')
    recommend_items = Purchases.getRecommend(Purchases, user_name)
    # 購入履歴取得
    purchased_items = []
    if user_name:
        purchased_items = Purchases.objects.filter(user_id=Users.objects.get(name=user_name).id)

    return render(request, 'index.html',
        {
            'users': users,
            'items': items,
            'recommend_items': recommend_items,
            'purchased_items': purchased_items,
            'selected': user_name
        }
    )


ここでGETで受け取ってます。

肝心のレコメンドの処理は



recommend_items = Purchases.getRecommend(Purchases, user_name)


こちらでmodel経由で取得しています。

・レコメンド処理

modelは以下です。
https://github.com/yueguchi/recommend-with-django/blob/master/recommend/models.py

1. requestパラメータとしてユーザ名を受け取る
2. ユーザーが購入した全ての商品を取得する



user_purchases = Purchases.objects.filter(user_id=Users.objects.get(name=user_name).id)


3. 全ての商品を取得し、購入者と紐付ける



# 全購入商品を商品ID => 複数ユーザーIDで束ねる
        item_user_buyer = {}
        for item in Items.objects.all():
            user_id_list = []
            for purchase_by_item in Purchases.objects.filter(item_id=item.id):
                user_id_list.append(purchase_by_item.user_id)
            item_user_buyer[item.id] = user_id_list


4. ユーザーの購入した商品を主軸にループさせ、全ての商品とのjaccard指数を算出する



# jaccard
        recommend_items = []
        already_item_id_list = user_purchases.values_list('item_id', flat=True)
        # ユーザーが購入した商品をベースにループさせ
        for key1 in purchased_items_other_users:
            # 全ての商品を比較させる
            ret = []
            for key2, item in item_user_buyer.items():
                ret.append(str(key2) + "," + str(self.__jaccard(purchased_items_other_users[key1], item)))
            recommend_items.append(self.__getUniqueRecommendItems(ret, already_item_id_list))


5. 最後にjaccard指数にかけて、レコメンド商品を返却する



def __jaccard(e1, e2):
        """
        A & &B集合 / A||B集合で近似値を測るアルゴリズム
        """
        set_e1 = set(e1)
        set_e2 = set(e2)
        return float(len(set_e1  & set_e2)) / float(len(set_e1 | set_e2))


レコメンドはこんな感じになってます。
ちょっとTODO残ってましたが、お許しください。
全体の雰囲気が伝わればと思いますm(__)m

・購入時の二重SUBMIT防止

今回のレコメンドとはあまり関係ないですが、商品購入時にPRGパターンを使うことで、二重に商品を購入する事象を回避しています。

view.pyの


def purchase(request):
    """
    商品ID、購入者名を元に購入処理を行う
    PRGパターンによる二重submitを抑止
    """
    item_id = request.POST['item_id']
    user_name = request.POST['user_name']
    if request.method == 'GET' or not item_id or not user_name:
        raise Exception('this url is not support "GET"')
    # 登録
    p = Purchases(item=Items.objects.get(id=item_id), user=Users.objects.get(name=user_name))
    p.save()
    messages.success(request, '購入しました!')
    return redirect('item-list')


ここです。

POSTで購入し、RedirectでクライアントにGETで再アクセスさせることで、ブラウザリロードしても
二重で商品が買われることを防いでいます。


djangoを使って見て

使いやすかったです。入門しやすいと言いますか。

最近のFWにありがちなREPLでしたり、migrationも、



./manage.py shell
./manage.py dbshell
./manage.py migrate


これらで簡単に使えちゃいますし。

localサーバも



./manage.py runserver


python修正時に自動で再起動かけてくれますし、スタイリッシュな開発が気持ちよくできますね。

ただscaffoldのような仕組みはないのかもしれないのですが(あったらごめんなさい)、django adminがあれば余り不便には感じないかと思います。

▼まとめ
・アイテムベース協調フィルタリング「jaccard指数」は手軽なのに、そこそこ精度が良い
・jaccard指数は集合の類似度でレコメンドするよ
django楽しい

▼課題
djangoのORMにおけるN+1問題の検証が未検証

商品とユーザーが増えれば、その分だけ膨大な計算が毎リクエストで実行されるため、
今回みたいにrequest <-> responseのやり取りの中でリアルタイムに計算させるものじゃない。
バッチなりでDBにレコメンド状態を永続化させておいて、毎時更新するなりして、画面に表示するか等の工夫が必要。


以上です!今回もありがとうございました!!