🗂

【個人開発 #1-5】レコメンドシステムを作ってみた vol.5 〜処理の実装〜

2023/12/08に公開

作りたいシステム

忙しい人のためのギフト提案型 AI「AiSAP!(エイサップ)」

忙しい社会人のために、AI が可能な限り素早く(ASAP: As Soon As Possible)、最適なギフトを提案してくれる Web サービス。

今回は処理の実装について説明していきます。
今回で最終回となります。

フォルダ構成

前回作成したフォルダ構成は下記のようになっています。

.
├── app.py
├── recommender.py
└── data
    ├── item.csv
    ├── rating.csv
    └── recipient.csv

データの作成(csv ファイルの作成)

まずはこちらで設計した、item, recipient, rating の 3 つのテーブルを csv ファイルで作成し、data フォルダ内に格納します。
下記リンク先の Github にサンプルを載せているのでダウンロードし、data フォルダ内に格納してください。

Github のリンク

処理の実装

次はいよいよ処理の実装に入ります。
いくつかのステップに分けて説明していきます。

1. 検索ボタンをトリガーに設定(app.py)

検索ボタンを押下した時に、レコメンドシステムが動くように設定します。
下記のようにボタンを if 文の条件にして、中に処理を書くとボタンがトリガーとして設定されます。
処理の内容としては、get_recommendations 関数(自作関数)でおすすめ商品を検索し、
取得した結果を for 文でループさせて、上位 3 商品のリンクと価格を表示しています。

# 検索ボタン押下時の処理
if st.button("検索"):
    # おすすめ商品を検索
    recommended_items = get_recommendations(searchReqList)

    # 取得した結果から上位3商品を表示
    for index, item in enumerate(recommended_items):
        link = f'[{index+1}.{item[0]}]({item[2]})'
        st.markdown(link, unsafe_allow_html=True)
        st.write(f'{item[1]}円')

また、get_recommendations 関数は recommender.py にて作成するので、app.py に import 文を書いておきます。

from recommender import get_recommendations

2. 受け取り手タイプを特定(recommender.py)

recommender.py を編集していきます。
まずはレコメンドを実行する get_recommendations 関数を定義して、引数に searchReqList を設定します。
これは、app.py から受け取る、画面で入力した検索条件が入ったリストです。

そして、入力した検索条件と合致する受け取り手タイプを recipient テーブル(ここでは csv ファイル)から探し、変数 recipient_type に格納します。
どんな相手に渡すのかを特定するわけです。

def get_recommendations(searchReqList):
  # 受け取り手データの読み込み
  recipient_data = pd.read_csv('./data/recipient.csv')

  # 受け取り手タイプをチェックする
  recipient_type = 0
  for index, data in recipient_data.iterrows():
      if searchReqList[0] == data["sex"] and searchReqList[1] == data["age_group"] and searchReqList[2] == data["relationship"] and searchReqList[3] == data["scene"]:
          recipient_type = data["recipient_type"]
          break

3. 評価行列を作成(recommender.py)

次は評価行列を作成します。
行に item_id、列に recipient_type を並べ、交わるところに評価値を埋めていきます。
「ネックレス は 、20 代女性の誕生日にあげるプレゼントとして、評価 5」といった感じです。
get_recommendations 関数の続きに書いていきます。

  # 評価データの読み込み
  rating_data = pd.read_csv('./data/rating.csv')

  # item_id × recipient_typeの行列を作成(とりあえずゼロで埋める)
  item_list = rating_data.sort_values('item_id').item_id.unique()
  recipient_list = rating_data.sort_values('recipient_type').recipient_type.unique()
  rating_matrix = np.zeros([len(item_list), len(recipient_list)])

  # それぞれのitem_idとrecipient_typeの組み合わせの時の評価値を埋める
  for item_id in range(1, len(item_list)+1):
      recipient_list = rating_data[rating_data['item_id'] == item_id].sort_values('recipient_type').recipient_type.unique()
      for type in recipient_list:
          try:
              rating = rating_data[(rating_data['item_id'] == item_id) & (rating_data['recipient_type'] == type)].loc[:, 'rating']
          except:
              rating = 0
          rating_matrix[item_id-1, type-1] = rating

上記で作成した行列 rating_matrix を出力すると下記のようになります。

[[-5. -2. -2. ...  7.  5. 15.]
 [-3. -5. -1. ...  6.  6. 13.]
 [ 0.  3.  4. ... -1. -1.  7.]
 ...
 [-7. -7. -6. ... 11. 11.  8.]
 [-8. -8. -7. ...  7.  7.  0.]
 [-3. -3. -3. ...  6.  6. 14.]]

これを表にするとこんなイメージです。
rating_matrix

4. 商品同士のコサイン類似度を計算(recommender.py)

ここまでで各商品がどんな受け取り手に喜ばれるのか/喜ばれないのかがわかりました。
これでいよいよ商品同士の類似度を計算して、レコメンドできるようになります。
今回は「コサイン類似度」を計算したいので、scikit-learn の pairwise_distance という関数を使いましょう。
この計算結果は、商品同士が似ていれば 1 に近づき、似ていなければ-1 に近づき、無関係であれば 0 に近づいていきます。

まずは下記の import 文を行頭に記載します。

import sklearn.metrics as skmt

そして、get_recommendations 関数の続きに計算処理を書きます。
ここで、同じ商品同士の類似度は 1 になってしまって 1 番におすすめされてしまうので、0 に書き換える処理も書いておきます。

    # scikit-learnのpairwise_distancesを用いてコサイン類似度行列を計算
    similarity_matrix = 1 - skmt.pairwise_distances(rating_matrix, metric='cosine')

    # 対角成分の値はゼロにする
    np.fill_diagonal(similarity_matrix, 0)

上記で作成した行列 similarity_matrix を出力すると下記のようになります。

[[0. 0.4437314 0.37590602 ... 0.64586167 0.5853507 0.65901553]
 [0.4437314 0. 0.21627518 ... 0.65265878 0.59924339 0.69970742]
 [0.37590602 0.21627518 0. ... 0.12124443 -0.0337501 0.09870698]
 ...
 [0.64586167 0.65265878 0.12124443 ... 0. 0.66391971 0.83788756]
 [0.5853507 0.59924339 -0.0337501 ... 0.66391971 0. 0.74424061]
 [0.65901553 0.69970742 0.09870698 ... 0.83788756 0.74424061 0.]]

これを表にするとこんなイメージです。
similarity_matrix

5. 商品を評価(recommender.py)

商品同士の類似度がわかったら、あとは「各商品の他の商品との類似度」×「特定した受け取り手タイプの各商品への評価」を算出して合計します。
すると、「高評価の商品に似ている商品」と「低評価の商品に似ていない商品」は高順位となります。

get_recommendations 関数の続きに下記処理を書きます。

    # 対象の受け取り手タイプの評価値を抜き出し、「類似度×評価点」を算出
    rating_matrix_recipient = rating_matrix[:, recipient_type - 1]
    pred_rating = similarity_matrix * rating_matrix_recipient

    # 商品(行)ごとに「類似度×評価点」を合計
    pred_rating_sum = pred_rating.sum(axis=1)

6. 商品情報を抽出・出力(recommender.py)

今回は上位 3 商品を出力したいので、item_id を取得し、それをもとに item.csv から商品情報を取得して app.py に返します。

get_recommendations 関数の続きに計算処理を書きます。

    # 上位3商品のitem_idをレコメンドリストに格納
    recommend_list = np.argsort(pred_rating_sum)[::-1][:3] + 1

    # 商品情報を取得する
    item_data = pd.read_csv('./data/item.csv')
    index_1 = recommend_list[0] - 1
    index_2 = recommend_list[1] - 1
    index_3 = recommend_list[2] - 1
    item_1 = [item_data.iloc[index_1,1], item_data.iloc[index_1,3], item_data.iloc[index_1,4]]
    item_2 = [item_data.iloc[index_2,1], item_data.iloc[index_2,3], item_data.iloc[index_2,4]]
    item_3 = [item_data.iloc[index_3,1], item_data.iloc[index_3,3], item_data.iloc[index_3,4]]

    recommended_items = [item_1, item_2, item_3]
    return recommended_items

以上で実装は完了です。

実行結果は下記のようになります。
※Github に公開するにあたって商品の URL や画像等のデータは削除したので、適宜追加して動かしてみてください。

レコメンド結果

ソースコードは下記を参照してください。
https://github.com/Waka07/AiSAP.git

まとめ

形にはしてみましたが、サービスと呼ぶには程遠いです。
正確なおすすめを出すことがいかに難しいかがわかりました。
ただ、推薦システムについて少しでも知れたことは良かったと思います。

参考文献

https://qiita.com/michi_wkwk/items/613034b1ec52b6be4720

Discussion