Python surprise で作る らくらく「レコメンドエンジン」(その4)
– 新規ユーザに対するレコメンド –

Python surprise で作る らくらく「レコメンドエンジン」(その4)– 新規ユーザに対するレコメンド –

協調フィルタリングやSVDなどの鉄板の「推薦システム(レコメンドエンジン)」であれば、PythonのSurpriseライブラリで簡単に作れます。

このライブラリは推薦アルゴリズムの学習と予測を簡単に行うことができます。

レコメンドエンジン(レコメンドモデル)を構築後に、新規ユーザに対しレコメンドを実施することがあります。

そのときの方法は主に2つあります。

  • 再度、レコメンドエンジンを学習しレコメンド
  • 学習済みのレコメンドエンジンを使いレコメンド

前者は、新規ユーザの情報や、既存ユーザの更新された情報を使うため、精度は高そうですが、ただただ時間がかかります

後者は、学習済みのモデルを使うため、すぐにでも新規ユーザに対するレコメンドがなされますが、データがやや古いです。

一長一短ありますが、前者の方法であれば、前回までの内容をそのまま適応すれば事足ります。

以下は前回の記事です。

Python surprise で作る らくらく「レコメンドエンジン」(その3)– AutoML(自動機械学習)的レコメンドエンジン構築 –

ということで今回は、後者の「学習済みのレコメンドエンジンを使い新規ユーザに対しレコメンドをする方法」についてお話しします。

その前に、レコメンドエンジンのコールドスタート問題に軽く触れます。

コールドスタート問題

新規ユーザーに対するレコメンデーションには、コールドスタート問題とも呼ばれる重要な問題があります。

ユーザーが新しくシステムに参加したばかりで、そのユーザーの過去の行動や好みに関する情報がないために起こる問題です。

この問題は、ユーザーベースまたはアイテムベースの協調フィルタリングのような手法では解決が難しいです。

新規ユーザーが少なくとも1度はアイテムを購入している場合(今回利用する例では1度でも映画の評価をしている場合)、その購入情報(今回は評価情報)を利用して協調フィルタリングのような手法を用いてレコメンデーションを行うことは可能です。

ユーザーが評価したアイテムから、そのユーザーの好みを推定し、類似のアイテムをレコメンデーションすることができるからです。

一方、何も購入していない新規ユーザー(そもそもユーザなのかという問題はありますが……)に対しては、内容ベースのフィルタリング(Content-Based Filtering)人気度ベースのレコメンデーションが適用されます。

内容ベースのフィルタリングでは、ユーザーが自身の興味・好みについて明示的に情報を提供することで、その情報に基づいたアイテムをレコメンデーションします。

人気度ベースのレコメンデーションは、全てのユーザーに対して人気のあるアイテムをレコメンデーションします。

また、デモグラフィックベースの推薦方法もあります。これは、年齢、性別、地域などのユーザーの人口統計学的な情報に基づいてレコメンデーションを行います。

例えば、これらの情報を説明変数に、回帰問題(目的変数:映画などの5段階評価など)もしくは分類問題(目的変数:購入の有無など)の予測モデルを構築し、好ましい結果になるアイテムをレコメンドします。

これらの方法は完全な解決策ではないかもしれませんが、新規ユーザーに対するレコメンデーションの一部を補完する方法として用いられます。

 

今回利用するデータと、新規ユーザのデータ

今回も前回と同じ、MovieLens データセットを利用します。

映画の評価データセットで、具体的には、3つの変数からなります。

  • user:ユーザーの識別子
  • item:アイテム(今回は、映画)の識別子
  • rating:評価値(今回は、1~5までの5段階評価)

 

今回の新規ユーザは、新規にユーザ登録し、少なくとも1度は映画に対し評価をしたユーザです。購買履歴データであれば、一度は購買経験のあるユーザです。

 

新規ユーザを、今回はPythonで乱数を用いてざっくり作ります。

具体的には、新規ユーザのIDは、既存ユーザより大きな値にしています。アイテムのIDは既存アイテムIDを利用しています。新規ユーザの評価アイテム数とその評価値は乱数で設定しています。

以下、コードです。

import pandas as pd
import numpy as np

# 新規ユーザ数を設定(ここでは50人)
num_new_users = 50

# 新規ユーザーごとに評価するアイテム数をランダムに設定(1~10の範囲)
num_ratings_per_user = np.random.randint(1, 11, size=num_new_users)

# 新規ユーザーのIDを設定(ここでは944から始まる)
new_user_ids = np.arange(944, 944 + num_new_users)

# アイテムIDの範囲を設定(ml-100k データセットに存在するアイテムのIDを使用)
item_ids = np.arange(1, 1683)

# サンプルデータを生成するための空のリストを作成
data = []
# 新規ユーザーごとに評価データを生成
for user_id, num_ratings in zip(new_user_ids, num_ratings_per_user):
    # ユーザーが評価するアイテムの数だけ繰り返し
    for _ in range(num_ratings):
        # アイテムIDをランダムに選択
        item_id = np.random.choice(item_ids)
        # 評価をランダムに生成(1~5の範囲)
        rating = np.random.choice([1, 2, 3, 4, 5])
        # ユーザーID、アイテムID、評価をリストに追加
        data.append([user_id, item_id, rating])

# データをDataFrameに変換(列名は'user_id', 'item_id', 'rating')
df = pd.DataFrame(data, columns=['user_id', 'item_id', 'rating'])

# DataFrameの内容を確認するために出力
print(df)

# DataFrameをCSVファイルに保存('new_user_ratings.csv'という名前で、インデックスは保存しない)
df.to_csv('new_user_ratings.csv', index=False)

 

以下、実行結果です。

     user_id  item_id  rating
0        944      214       4
1        944      194       1
2        944     1101       5
3        944      350       5
4        944     1177       1
..       ...      ...     ...
280      992      530       1
281      992      277       1
282      993     1016       2
283      993     1158       2
284      993     1603       1

[285 rows x 3 columns]

 

外部ファイル(’new_user_ratings.csv’)に保存するようにしています。

 

レコメンドエンジンの構築

実務運用時には、最初からレコメンドエンジンが構築されていると思いますが、今回は手元にないので既存ユーザでレコメンドエンジンを構築していきます。

前々回の記事とほぼ同じです。

 

先ず、必要なモジュールを読み込みます。

以下、コードです。

import pandas as pd
from surprise import SVD
from surprise import Dataset
from surprise import dump
from surprise.model_selection import GridSearchCV

 

次にデータセットを読み込みます。先ほども言いましたが、前回と同じMovieLensのデータセットです。

以下、コードです。

# ml-100kデータセットをロード
data = Dataset.load_builtin('ml-100k')

 

推薦アルゴリズムとしてSVDを使います。ハイパーパラメータがいくつかありますので、チューニングします。

以下、コードです。

# グリッドサーチのパラメータを設定
param_grid = {
    'n_epochs': [5, 10, 20, 50], 
    'lr_all': [0.002, 0.005, 0.01],
    'reg_all': [0.02, 0.1, 0.2, 0.4, 0.6],
    'n_factors': [50, 100, 200]
}

# SVDアルゴリズムと設定したパラメータでグリッドサーチを実施
gs = GridSearchCV(SVD, param_grid, measures=['rmse', 'mae'], cv=10)
gs.fit(data)

# ベストなパラメータとその時のRMSE, MAEを出力
print(f"Best Parameters: {gs.best_params['rmse']}")
print(f" - RMSE: {gs.best_score['rmse']}")
print(f" - MAE: {gs.best_score['mae']}")

 

以下、実行結果です。

Best Parameters: {'n_epochs': 50, 'lr_all': 0.01, 'reg_all': 0.1, 'n_factors': 200}
 - RMSE: 0.9047285834697482
 - MAE: 0.7142858016198088

 

このベストなハイパーパラメータで、レコメンドエンジンを構築します。

以下、コードです。

# ベストなパラメータを用いてSVDアルゴリズムを初期化
algo = SVD(
    n_epochs=gs.best_params['rmse']['n_epochs'], 
    lr_all=gs.best_params['rmse']['lr_all'], 
    reg_all=gs.best_params['rmse']['reg_all'], 
    n_factors=gs.best_params['rmse']['n_factors']
)

# 全データを学習データとしてモデルを学習
trainset = data.build_full_trainset()
algo.fit(trainset)

# 各ユーザの評価を実施
testset = trainset.build_anti_testset()
predictions = algo.test(testset)

 

構築したら、外部ファイルとして保存します。

以下、コードです。

save_path = "model.pkl"
dump.dump(save_path, predictions=predictions, algo=algo)

 

構築したモデルは、外部ファイル(”model.pkl”)に保存します。

このファイルには、以下の情報が含まれている。

  • 学習済みの「レコメンドエンジン」(algoの中)
  • 学習で利用した「学習データ」(algoの中)
  • 学習データの各ユーザ(既存ユーザ)の評価結果(predictions)

 

この外部ファイル(”model.pkl”)を読み込んで、新規ユーザに対しレコメンドを実施します。

 

新規ユーザに対しレコメンドをする

 必要なモジュールの読み込み

先ず、必要なモジュールを読み込みます。

以下、コードです。

import pandas as pd
from surprise import dump

 

 学習済みのレコメンドエンジンを読み込む

学習済みのレコメンドエンジンを読み込みます。

以下、コードです。読み込む外部ファイルは「”model.pkl”」です。

# 学習済みのレコメンドエンジンをロード
load_path = "model.pkl"
predictions, loaded_algo = dump.load(load_path)
trainset = loaded_algo.trainset

 

以下に格納されています。

  • loaded_algo:学習済みの「レコメンドエンジン」
  • trainset:学習で利用した「学習データ」
  • predictions:学習データの各ユーザ(既存ユーザ)の評価結果

 

 新規ユーザの履歴情報獲得

新規ユーザの情報(今回は、映画の評価データ)を読み込みます。

以下、コードです。

# 新規ユーザの評価データをロード
new_user_ratings = pd.read_csv('new_user_ratings.csv')

# 新規ユーザの一覧を取得
unique_users = new_user_ratings['user_id'].unique()

 

 新規ユーザに対するレコメンド(TOP10)

次に、新規ユーザに対するレコメンドを実施します。お勧めTOP10です。

以下、コードです。

# レコメンデーションの結果を保存するための空のリストを作成
recommendations = []

# 各新規ユーザごとにレコメンデーションを行う
for user in unique_users:
    # 当該ユーザの評価データを取得
    user_ratings = new_user_ratings[new_user_ratings['user_id'] == user]
    # 評価済みのアイテムIDをリストとして取得
    rated_items = user_ratings['item_id'].values.tolist()

    # 当該ユーザの評価予測を行う
    preds = []
    for iid in trainset.all_items():
        # 評価済みのアイテムは除外
        if trainset.to_raw_iid(iid) not in rated_items:
            # 未評価のアイテムに対する評価予測を行い、結果をリストに保存
            preds.append((user, trainset.to_raw_iid(iid), loaded_algo.predict(user, trainset.to_raw_iid(iid)).est))
    
    # 評価予測の結果を予測評価の降順にソート
    preds.sort(key=lambda x: x[2], reverse=True)

    # レコメンデーションの結果をDataFrameに変換し、リストに保存
    recommendations.append(pd.DataFrame(preds[:10], columns=['user_id', 'item_id', 'rating']))

# 全新規ユーザのレコメンデーション結果を連結
recommendations = pd.concat(recommendations)

# レコメンデーション結果を確認
print(recommendations)

 

以下、実行結果です。

    user_id item_id    rating
0       944    1449  4.594602
1       944     318  4.393487
2       944      64  4.373702
3       944     169  4.349784
4       944     408  4.326687
..      ...     ...       ...
5       993     483  4.288965
6       993    1500  4.287804
7       993      12  4.285650
8       993     114  4.279630
9       993     603  4.275492

[500 rows x 3 columns]

 

 レコメンド結果を保存

最後に、レコメンド結果をCSVファイルとして保存します。

以下、コードです。

# レコメンデーション結果をCSVファイルに保存
recommendations.to_csv('new_user_recommendations.csv', index=False)

 

外部ファイル(’new_user_recommendations.csv’)として、新規ユーザに対するレコメンドを保存しています。

 

 既存ユーザに対するレコメンド結果

predictionsに、学習データの各ユーザ(既存ユーザ)の評価結果が格納されてます。

折角なので、既存ユーザに対するレコメンド(TOP10)も抽出し保存します。

以下、コードです。

from collections import defaultdict

def get_top_n(predictions, n=10):
    # 各ユーザーの評価を保持するためのリスト
    rows = []
    
    # ユーザーIDをキーとして、アイテムの評価をリストとして保存
    user_ratings = defaultdict(list)
    
    # 各ユーザーのすべてのアイテム評価を集める
    for uid, iid, true_r, est, _ in predictions:
        user_ratings[uid].append((iid, est))
    
    # 各ユーザーのトップnの評価を選択し、rowsリストに追加
    for uid, ratings in user_ratings.items():
        # 評価値で降順にソート
        ratings.sort(key=lambda x: x[1], reverse=True)
        top_ratings = ratings[:n]
        for iid, est in top_ratings:
            rows.append([uid, iid, est])
    
    # rowsリストからデータフレームを作成して返す
    return pd.DataFrame(rows, columns=['User', 'Item', 'Estimate'])

# トップnの予測をデータフレーム形式で取得
top_n_df = get_top_n(predictions, n=10)

 

最後に、CSVファイルとして保存します。

以下、コードです。

# レコメンデーション結果をCSVファイルに保存
top_n_df.to_csv('existing_user_recommendations.csv', index=False)

 

まとめ

今回は、学習済みのレコメンドエンジンを使い、新規ユーザに対しレコメンドをしました。

Pythonコードを毎回実行するのも面倒なので、次回はStreamlitで簡易的なWebアプリを作ります。

Python surprise で作る らくらく「レコメンドエンジン」(その5)– Streamlitで作る簡易「レコメンドWebアプリ」 –