Python surprise で作る らくらく「レコメンドエンジン」(その2)
– ハイパーパラメータ調整しレコメンドエンジンを構築 –

Python surprise で作る らくらく「レコメンドエンジン」(その2)– ハイパーパラメータ調整しレコメンドエンジンを構築 –

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

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

前回、 Surprise でレコメンドエンジンの簡単な作り方について説明しました。

Python surprise で作る らくらく「レコメンドエンジン」(その1)– さくっと Surprise でレコメンドエンジンを作ってみよう! –

推薦アルゴリズムの中には、ハイパーパラメータを持つものがあり、適切なチューニングをした方がよい場合があります。

今回は、Surprise で ハイパーパラメータ調整しレコメンドエンジンを構築する方法について説明します。

前回と同様に、SVD (Singular Value Decomposition)でレコメンドエンジンを構築していきます。

SVD (Singular Value Decomposition)とは?

SVDについて簡単に説明をします。面倒な方は読み飛ばしてください。

 

SVDは、行列を特定の形に分解する一種の行列分解技術で、数学、物理学、コンピュータサイエンスなど、多くの分野で利用されています。

推薦システムの分野においては、ユーザーとアイテム間の評価(評価行列)を分解し、それに基づいて欠損値を推定するために使用されます。

 

具体的には、m \times n の評価行列 R を以下の3つの行列の積に分解します。

R = U \times \Sigma \times V^T

ここで……

  • mはユーザーの数
  • nはアイテムの数
  • rは潜在的な特徴または因子の数

……としたとき……

  • m \times r のユーザ特徴行列 U各行はユーザーを、各列は潜在的な特徴(または因子)を表します。つまり、この行列は各ユーザーがどの程度各潜在特徴を持つかを示します。
  • r \times r の対角行列 \Sigma対角要素は特異値と呼ばれます。特異値は、それぞれの潜在特徴が評価行列R の再現にどの程度寄与するかを示します。
  • n \times r のアイテム特徴行列 V^T各行はアイテムを、各列は潜在的な特徴(または因子)を表します。つまり、この行列は各アイテムがどの程度各潜在特徴を持つかを示します。

 

この分解により、評価行列 R がもつユーザーとアイテムの潜在的な特徴を捉え、それに基づいて評価値を予測することが可能になります。

つまり、ユーザーがまだ評価していないアイテムに対する評価値を予測し、それに基づいてユーザーにアイテムを推薦することが可能になります。

 

また、SVDにはいくつかの派生形が存在し、特定の問題に対してより効率的に対処するための改良が加えられています。

例えば、SVD++は、ユーザーが評価したアイテムと評価していないアイテムの両方を考慮に入れることで、より精度の高い推薦を行います。

 

SurpriseのSVDのハイパーパラーメータ

SurpriseのSVDアルゴリズムは以下の主要なハイパーパラメータを持っています。

  • n_factors: 潜在因子の数。これは分解後のユーザ特徴行列とアイテム特徴行列の幅を表します。多すぎると過学習を引き起こし、少なすぎると表現力が不足します。
  • n_epochs: SGD(確率的勾配降下法)のエポック数。エポック数が多いほど、モデルは学習データに対して学習しますが、過学習の可能性もあります。
  • lr_all: 全てのパラメータに対する学習率。学習率はSGDのステップサイズを制御します。大きすぎると最適な解を見逃し、小さすぎると学習が遅くなります。
  • reg_all: 全てのパラメータに対する正則化項。正則化は過学習を防ぐための手法で、値が大きいほどパラメータの値を抑制します。

他にもバイアス項や各パラメータの初期値に関するハイパーパラメータなど、細かな設定を行うことができます。

適切なパラメータを見つけるためには、グリッドサーチやランダムサーチなどのハイパーパラメータ最適化手法を使用して、異なるパラメータの組み合わせを試すことが一般的です。

今回は、Surpriseの中に実装されている、グリッドサーチ(全探索)機能を使い、パイパーパラメータチューニングを実施します。

 

推薦システム(レコメンドエンジン)を作る

 流れ

取り急ぎ、以下の手順で作って行きます。

  1. 必要なモジュールの読み込み
  2. データセットの読み込み(前回と同じMovieLens)
  3. ハイパーパラメータの範囲を定義
  4. ハイパーパラメータチューニングの実施
  5. レコメンドの実施

 

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

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

以下、コードです。

from surprise import SVD
from surprise import Dataset
from surprise.model_selection import GridSearchCV

 

  • from surprise import SVD: surpriseライブラリからSVDという名前のアルゴリズムをインポートします。このSVDは特異値分解と呼ばれる手法を用いた協調フィルタリングの推薦アルゴリズムです。
  • from surprise import Dataset: Datasetsurpriseライブラリに含まれるデータセットローダーで、標準的な推薦アルゴリズム向けのデータセットを扱うためのツールです。Datasetクラスを通じて、surpriseライブラリが提供する内蔵データセット(例えば、MovieLensデータセット)を読み込んだり、自分で作成したデータセットをロードしたりすることが可能です。
  • from surprise.model_selection import GridSearchCV: GridSearchCVはハイパーパラメータチューニングを行うためのクラスで、与えられた範囲のハイパーパラメータについて、全ての組み合わせでモデルの性能を評価し、最も良い結果を出すパラメータの組み合わせを探します。CVはCross-Validation(交差検証)を表し、データセットを複数の部分に分割し、その一部をテストデータ、残りを学習データとしてモデルの性能を評価する方法を指します。

 

 データセットの読み込み

次に、MovieLens データセットをロードします。前回利用したものと同じデータセットです。

以下、コードです。

data = Dataset.load_builtin("ml-100k")

 

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

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

 

 ハイパーパラメータの範囲を定義

SVDアルゴリズムのハイパーパラメータをチューニングするための、パラメータの探索範囲を定義します。

以下、コードです。

param_grid = {
    'n_epochs': [5, 10, 20, 30],
    'lr_all': [0.002, 0.005, 0.01, 0.02],
    'reg_all': [0.4, 0.6, 0.8],
    'n_factors': [50, 100, 150] 
}

 

各ハイパーパラメータと探索範囲を以下のように設定しました。

  • 'n_epochs': SGD(確率的勾配降下法)のエポック数(すなわち、全データに対する反復回数)。この例では、5, 10, 20, 30の4つの値を探索範囲に設定しています。
  • 'lr_all': 全てのパラメータの学習率(SGDのステップサイズ)。この例では、0.002, 0.005, 0.01, 0.02の4つの値を探索範囲に設定しています。
  • 'reg_all': 全てのパラメータの正則化項の強さ(過学習を抑制するための項)。この例では、0.4, 0.6, 0.8の3つの値を探索範囲に設定しています。
  • 'n_factors': 潜在因子の数(つまり、分解により得られるユーザー特徴行列とアイテム特徴行列の列数)。この例では、50, 100, 150の3つの値を探索範囲に設定しています。

 

これらのパラメータをすべて組み合わせることで、多数の異なるモデルが生成され、その中で最も性能の良いパラメータの組み合わせを探していきます。

 

 ハイパーパラメータチューニングの実施

GridSearchCVを使い、先ほど指定したハイパーパラメータの組み合わせをすべて試し最適なパラメータを探す、「グリッドサーチ」を行います。

この最適なハイパーパラメータの探索で、どのハイパーパラメータの組み合わせが良いのかを評価するとき、CV(Cross Validation、交差検証)で行います。

交差検証とは、データを複数の部分に分割し、その一部をテストデータ、残りを学習データとして使用し、この分割を変えながらモデルの性能を評価する方法で、データセット全体でのモデルの性能をより正確に評価することができます。

以下、コードです。

gs = GridSearchCV(SVD, param_grid, measures=['rmse', 'mae'], cv=10)
gs.fit(data)

 

  • gs = GridSearchCV(SVD, param_grid, measures=['rmse', 'mae'], cv=10): GridSearchCVは、グリッドサーチクラスのインスタンスを作成します。このクラスは、指定されたパラメータグリッド(ここではparam_grid)上でのモデル(ここではSVD)の性能を評価するためのクロスバリデーションを実施します。評価指標は平均絶対誤差(MAE)と平方根平均二乗誤差(RMSE)の二つで、クロスバリデーションは10分割で行います。
  • gs.fit(data): この行は、指定されたデータ(ここではdata)に対してグリッドサーチを実施します。これにより、各パラメータの組み合わせについて、モデルの学習と評価(クロスバリデーション)が行われます。

 

このプロセスの結果、グリッドサーチは最適なハイパーパラメータの組み合わせ(つまり、評価指標が最も高くなるハイパーパラメータ)を見つけ出すことができます。

この最適なハイパーパラメータは、後に新しいデータの予測に用いるモデルを学習する際に使用されます。

 

では、チューニング結果を見てみます。

以下、コードです。

# RMSEスコアで最良だったパラメータ
print('best params:',gs.best_params['rmse'])

# スコア
print('RMSE:',gs.best_score['rmse'])
print('MAE:',gs.best_score['mae'])

 

以下、実行結果です。

best params: {'n_epochs': 30, 'lr_all': 0.005, 'reg_all': 0.4, 'n_factors': 50}
RMSE: 0.9545505777539021
MAE: 0.7644204125258656

 

 レコメンドの実施

全ユーザに対しレコメンドを実施します。

先ず、GridSearchCVによるハイパーパラメータチューニングの結果から、最も良いパフォーマンス(最小のRMSE値)を達成したモデルを取得します。

以下、コードです。

best_model = gs.best_estimator['rmse']

 

次に、レコメンド構築用のデータセットと、レコメンド用のデータセットを作ります。

以下、コードです。

trainset = data.build_full_trainset()
testset = trainset.build_anti_testset()

 

  • data.build_full_trainset(): このメソッドは全てのデータからレコメンド構築用のデータセットを構築します。ユーザーとアイテムの評価データ全体を含みます。これを使用することで、全てのデータを使ってモデルを学習できます。
  • trainset.build_anti_testset(): このメソッドはユーザーとアイテムの評価がまだされていない組み合わせからなるデータセットを構築します。つまり、ユーザーがまだ評価していないアイテムに対する予測を作成するためのデータセットです。評価値が不明なアイテムに対して、レコメンドエンジンがどのような評価を予測するために使用されます。

これらの2つのメソッドを組み合わせることで、全てのデータを用いてレコメンドエンジンを学習し(data.build_full_trainset())、そのレコメンドエンジンを使用してまだ評価されていないアイテムに対する評価を予測する(trainset.build_anti_testset())ことができます。この結果を基にユーザーに対する新たなレコメンデーションを生成することが可能になります。

 

レコメンドエンジンを学習します。

以下、コードです。

best_model.fit(trainset)

 

では、各ユーザーに対して映画のレコメンドを行います。

各ユーザの評価を予測します。

以下、コードです。

predictions = best_model.test(testset)

 

上位nまで抜き出し、取り扱いやすいように辞書形式で格納します。

以下、コードです。

from collections import defaultdict

def get_top_n(predictions, n=10):
    top_n = defaultdict(list)
    for uid, iid, true_r, est, _ in predictions:
        top_n[uid].append((iid, est))

    for uid, user_ratings in top_n.items():
        user_ratings.sort(key=lambda x: x[1], reverse=True)
        top_n[uid] = user_ratings[:n]

    return top_n

top_n = get_top_n(predictions, n=10)

 

各ユーザーに対する上位n個のアイテムを予測評価値の高い順に抽出し、ユーザーごとにリストとしてtop_nに格納しました。

このtop_nは、ユーザーIDをキーとし、その値がユーザーにレコメンドされるアイテムのIDと予測評価のリストである辞書です。

これで完成です。

 

ただ、辞書ではなくデータフレームに変換したいな、と思う方もいるかと思います。

以下で、辞書からデータフレームに変換します。

import pandas as pd

def get_top_n_df(top_n):
    top_n_df = []
    for uid, user_ratings in top_n.items():
        for iid, est in user_ratings:
            top_n_df.append([uid, iid, est])

    return pd.DataFrame(top_n_df, columns=['User', 'Item', 'Estimate'])

top_n_df = get_top_n_df(top_n)

print(top_n_df) #確認

 

以下、実行結果です。

     User  Item  Estimate
0     196   408  4.258537
1     196  1449  4.253416
2     196   169  4.228119
3     196   318  4.218697
4     196    64  4.192698
...   ...   ...       ...
9425  941   483  4.435316
9426  941    12  4.424529
9427  941   603  4.400616
9428  941   178  4.390634
9429  941    50  4.387167

[9430 rows x 3 columns]

 

ちなみに、特定のユーザとアイテム(映画)を指定し、評価の予測するときには、以下のようにします。

uid = str(597) # raw user id
iid = str(1152) # raw item id
pred = best_model.predict(uid, iid, verbose=True)

 

以下、実行結果です。

user: 597        item: 1152       r_ui = None   est = 3.60   {'was_impossible': False}
  • user: 597 : ユーザーIDが597
  • item: 1152 : アイテムIDが1152
  • r_ui = None : 実際の評価値(ユーザーがアイテムに付けた評価)です。引数で設定していないのでNone
  • est = 3.60 : 推定値(予測された評価値)。つまり、アルゴリズムはユーザー597がアイテム1152に対して3.60の評価をすると予測
  • {'was_impossible': False} : 予測が不可能だったかどうかを示すフラグ。ここではFalseなので、予測が可能だった

 

まとめ

今回は、Surprise で ハイパーパラメータ調整しレコメンドエンジンを構築する方法について説明しました。

前回と同様に、SVD (Singular Value Decomposition)でレコメンドエンジンを構築しました。

SVD以外にも、色々なアルゴリズムがあります。

次回は、どのアルゴリズムがいいのかを自動検索する方法について説明します。

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