ここまでは顧客間類似度に基づいた推薦システムのアルゴリズムを理解して,そのアルゴリズムを実装してきました.しかしながら,前のページで実装したアルゴリズムは効率的な書き方になっていない部分もあることから,大きなデータで実行すると膨大なメモリを消費するとともに,長時間の計算が必要になることでしょう.ここでは,Surprise という推薦システムのライブラリの使用例を示します.
Surprise では推薦システムのための様々なモデルを利用することができます.ただし,surprise では Numpy のバージョンが1系でなければなりません.2024年6月に Numpy がバージョンアップしたことから,バージョン2系の Numpy がインストールされることが多くなっています.このため,Python の仮想環境を新たに作成し,Numpy のバージョン1系をインストールして,surprise, jupyter lab,pandas をインストールすることにします.
まず,インストールされている仮想環境を一覧表示します.
conda env list ⏎
  まだ surprise 用の仮想環境は作成されていないはずなので,Python 3.11 をベースにした仮想環境を作成し,その名前を py311surprise にします.
conda create -n py311surprise python=3.11 ⏎
もう一度,仮想環境を一覧で表示すると,作成した仮想環境が表示されるはずです.
conda env list ⏎
  作成した仮想環境 py311surprise を有効にします.
conda activate py311surprise ⏎
仮想環境にバージョン1系の numpy をインストールします.
pip install "numpy<2" ⏎
その後,surprise, jupyter lab, pandas をインストールします.
pip install surprise ⏎ pip install jupyter lab ⏎ pip install pandas ⏎
Jupyter Lab を起動します.
jupyter lab ⏎
Surprise のインストールができれば,インポートします.
import pandas as pd
import numpy as np
from surprise import Dataset, Reader, KNNWithMeans
np.set_printoptions(precision=3)    # 小数点以下の表示桁数を設定
np.set_printoptions(suppress=True)  # 指数表示を行わないように
次に,サンプルデータを GitHub から読み込み,データフレームに格納します.このとき,行列形式のデータではなく,次のような形式のデータを読み込んでいることに注意指定ください.
url_ratings = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_ratings.csv?raw=true"
df_ratings = pd.read_csv(url_ratings)
print(df_ratings)
customer item rating 0 8 3 2 1 1 1 2 2 6 1 3 3 9 2 2 ...(中略)... 35 7 0 4 36 9 0 3 37 4 3 4 38 4 1 3
surprise ではレイティングのスケールをあらかじめ設定する必要があります.Reader を使って rating の最小値 1 と最大値 5を次のように指定します.
reader = Reader(rating_scale=(1, 5))
列名を指定してデータフレームからデータセットを読み込みます.
data = Dataset.load_from_df(df_ratings[["customer", "item", "rating"]], reader)
  読み込んだ data にどのような形式で値が格納されているか確認しておきます.
vars(data)
{'reader': <surprise.reader.Reader at 0x11291d490>,
 'has_been_split': False,
 'df':     customer  item  rating
 0          8     3       2
 1          1     1       2
 2          6     1       3
 3          9     2       2
  ...(中略)...
 35         7     0       4
 36         9     0       3
 37         4     3       4
 38         4     1       3,
 'raw_ratings': [(8, 3, 2.0, None),
  (1, 1, 2.0, None),
  (6, 1, 3.0, None),
  (9, 2, 2.0, None),
    ...(中略)...
  (7, 0, 4.0, None),
  (9, 0, 3.0, None),
  (4, 3, 4.0, None),
  (4, 1, 3.0, None)]}
今回はすべてのレイティングデータを学習データとして設定します.
trainset = data.build_full_trainset()
設定した学習データがどのような形式になっているかも確認しておきます.
vars(trainset)
{'ur': defaultdict(list,
             {0: [(0, 2.0), (3, 5.0), (4, 1.0), (1, 5.0), (2, 4.0)],
              1: [(1, 2.0), (2, 4.0), (0, 2.0), (4, 3.0)],
              2: [(1, 3.0), (3, 3.0), (4, 1.0), (2, 2.0)],
              3: [(2, 2.0), (0, 2.0), (4, 2.0), (3, 3.0)],
              4: [(2, 4.0), (1, 3.0), (3, 2.0)],
              5: [(2, 3.0), (3, 4.0), (0, 4.0), (1, 3.0)],
              6: [(2, 2.0), (1, 3.0), (4, 2.0), (3, 2.0)],
              7: [(3, 5.0), (1, 4.0), (2, 3.0)],
              8: [(4, 3.0), (0, 2.0), (3, 2.0), (1, 5.0)],
              9: [(2, 5.0), (1, 4.0), (4, 5.0), (3, 4.0)]}),
 'ir': defaultdict(list,
             {0: [(0, 2.0), (8, 2.0), (1, 2.0), (3, 2.0), (5, 4.0)],
              1: [(1, 2.0),
               (2, 3.0),
               (4, 3.0),
               (7, 4.0),
               (9, 4.0),
               (8, 5.0),
               (6, 3.0),
               (0, 5.0),
               (5, 3.0)],
              2: [(3, 2.0),
               (4, 4.0),
               (1, 4.0),
               (5, 3.0),
               (6, 2.0),
               (9, 5.0),
               (7, 3.0),
               (0, 4.0),
               (2, 2.0)],
              3: [(2, 3.0),
               (0, 5.0),
               (7, 5.0),
               (5, 4.0),
               (8, 2.0),
               (4, 2.0),
               (6, 2.0),
               (9, 4.0),
               (3, 3.0)],
              4: [(8, 3.0),
               (0, 1.0),
               (2, 1.0),
               (6, 2.0),
               (1, 3.0),
               (3, 2.0),
               (9, 5.0)]}),
 'n_users': 10,
 'n_items': 5,
 'n_ratings': 39,
 'rating_scale': (1, 5),
 '_raw2inner_id_users': {8: 0,
  1: 1,
  6: 2,
  9: 3,
  2: 4,
  4: 5,
  3: 6,
  0: 7,
  5: 8,
  7: 9},
 '_raw2inner_id_items': {3: 0, 1: 1, 2: 2, 0: 3, 4: 4},
 '_global_mean': None,
 '_inner2raw_id_users': None,
 '_inner2raw_id_items': None}
上の結果の中に,学習データ内のユーザ数やアイテム数が格納されていることがわかったので,その値を表示してみます.
print(f"number of trainset customers = {trainset.n_users}")
print(f"number of trainset items     = {trainset.n_items}")
number of trainset customers = 10 number of trainset items = 5
  また,学習データは元のデータと必ずしも同じ並び順ではありません.次のコードは必ずしも必要ありませんが,学習データを行列形式に変換したものと,元の順番に並べたものを表示してみます.学習データと元のデータの並び順の関係は,上の結果にある _raw2inner_id_users や _raw2inner_id_items を手がかりに得られます.
trainset_matrix = np.zeros((trainset.n_users, trainset.n_items))
raw_matrix = np.zeros((trainset.n_users, trainset.n_items))  # 元のデータ
trainset_matrix[:,:] = np.nan
raw_matrix[:,:] = np.nan
for u, vals in trainset.ur.items():
  for i, rating in vals:
    # print(u, i, rating)
    trainset_matrix[u, i] = rating
    raw_matrix[trainset.to_raw_uid(u), trainset.to_raw_iid(i)] = rating
print(trainset_matrix)
print("-"*25)
print(raw_matrix)
[[ 2. 5. 4. 5. 1.] [ 2. 2. 4. nan 3.] [nan 3. 2. 3. 1.] [ 2. nan 2. 3. 2.] [nan 3. 4. 2. nan] [ 4. 3. 3. 4. nan] [nan 3. 2. 2. 2.] [nan 4. 3. 5. nan] [ 2. 5. nan 2. 3.] [nan 4. 5. 4. 5.]] ------------------------- [[ 5. 4. 3. nan nan] [nan 2. 4. 2. 3.] [ 2. 3. 4. nan nan] [ 2. 3. 2. nan 2.] [ 4. 3. 3. 4. nan] [ 2. 5. nan 2. 3.] [ 3. 3. 2. nan 1.] [ 4. 4. 5. nan 5.] [ 5. 5. 4. 2. 1.] [ 3. nan 2. 2. 2.]]
  顧客間の類似度はピアソン相関係数を利用しますが,そのためのオプションを設定します.なお,user_based を False にするとアイテム間の類似度を求めることになるので,全く異なった結果になってしまうことに注意してください.また,min_support は対象となる2人の顧客間で評価値を持つアイテム数が min_support 未満のときには相関係数を 0 にします.今回はそもそも小さなデータセットなので min_support = 0 にしています.
sim_options = {
    "name": "pearson",
    "user_based": True,
    "min_support": 0,
}
  いよいよモデルを設定して当てはめを行います.考慮対象とする近傍顧客数は k = 3 とします.
algo = KNNWithMeans(k=3, sim_options=sim_options)
algo.fit(trainset)
Computing the pearson similarity matrix... Done computing similarity matrix. <surprise.prediction_algorithms.knns.KNNWithMeans at 0x114fbfa50>
どのような結果が格納されているか確認してみましょう.
vars(algo)
{'bsl_options': {},
 'sim_options': {'name': 'pearson', 'user_based': True, 'min_support': 0},
 'verbose': True,
 'k': 3,
 'min_k': 1,
 'trainset': <surprise.trainset.Trainset at 0x117a99a10>,
 'bu': None,
 'bi': None,
 'n_x': 10,
 'n_y': 5,
 'xr': defaultdict(list,
             {0: [(0, 2.0), (3, 5.0), (4, 1.0), (1, 5.0), (2, 4.0)],
              1: [(1, 2.0), (2, 4.0), (0, 2.0), (4, 3.0)],
              2: [(1, 3.0), (3, 3.0), (4, 1.0), (2, 2.0)],
              3: [(2, 2.0), (0, 2.0), (4, 2.0), (3, 3.0)],
              4: [(2, 4.0), (1, 3.0), (3, 2.0)],
              5: [(2, 3.0), (3, 4.0), (0, 4.0), (1, 3.0)],
              6: [(2, 2.0), (1, 3.0), (4, 2.0), (3, 2.0)],
              7: [(3, 5.0), (1, 4.0), (2, 3.0)],
              8: [(4, 3.0), (0, 2.0), (3, 2.0), (1, 5.0)],
              9: [(2, 5.0), (1, 4.0), (4, 5.0), (3, 4.0)]}),
 'yr': defaultdict(list,
             {0: [(0, 2.0), (8, 2.0), (1, 2.0), (3, 2.0), (5, 4.0)],
              1: [(1, 2.0),
               (2, 3.0),
               (4, 3.0),
               (7, 4.0),
               (9, 4.0),
               (8, 5.0),
               (6, 3.0),
               (0, 5.0),
               (5, 3.0)],
              2: [(3, 2.0),
               (4, 4.0),
               (1, 4.0),
               (5, 3.0),
               (6, 2.0),
               (9, 5.0),
               (7, 3.0),
               (0, 4.0),
               (2, 2.0)],
              3: [(2, 3.0),
               (0, 5.0),
               (7, 5.0),
               (5, 4.0),
               (8, 2.0),
               (4, 2.0),
               (6, 2.0),
               (9, 4.0),
               (3, 3.0)],
              4: [(8, 3.0),
               (0, 1.0),
               (2, 1.0),
               (6, 2.0),
               (1, 3.0),
               (3, 2.0),
               (9, 5.0)]}),
 'sim': array([[ 1.   ,  0.   ,  0.966,  0.73 , -0.866, -0.408,  0.44 ,  0.866,  0.343, -0.762],
        [ 0.   ,  1.   , -0.5  ,  0.   ,  1.   , -0.5  , -0.866, -1.   , -0.189,  0.866],
        [ 0.966, -0.5  ,  1.   ,  0.866, -0.866,  0.5  ,  0.522,  0.866, 0.189, -0.905],
        [ 0.73 ,  0.   ,  0.866,  1.   , -1.   ,  0.5  ,  0.   ,  1.   , -0.5  , -1.   ],
        [-0.866,  1.   , -0.866, -1.   ,  1.   , -0.866,  0.   , -1.   ,  1.   ,  0.866],
        [-0.408, -0.5  ,  0.5  ,  0.5  , -0.866,  1.   , -0.5  ,  0.866, -1.   , -0.5  ],
        [ 0.44 , -0.866,  0.522,  0.   ,  0.   , -0.5  ,  1.   ,  0.   ,  0.945, -0.577],
        [ 0.866, -1.   ,  0.866,  1.   , -1.   ,  0.866,  0.   ,  1.   , -1.   , -0.866],
        [ 0.343, -0.189,  0.189, -0.5  ,  1.   , -1.   ,  0.945, -1.   ,  1.   , -0.189],
        [-0.762,  0.866, -0.905, -1.   ,  0.866, -0.5  , -0.577, -0.866, -0.189,  1.   ]]),
 'means': array([3.4 , 2.75, 2.25, 2.25, 3.  , 3.5 , 2.25, 4.  , 3.  , 4.5 ])}
  上の結果を見ると,sim に相関行列が格納されていそうなので表示してみます.しかしながら,この結果は元のデータとは順序が入れ替わっているので,値を見て直接的に理解をすることは困難です.
print(algo.sim)
[[ 1. 0. 0.966 0.73 -0.866 -0.408 0.44 0.866 0.343 -0.762] [ 0. 1. -0.5 0. 1. -0.5 -0.866 -1. -0.189 0.866] [ 0.966 -0.5 1. 0.866 -0.866 0.5 0.522 0.866 0.189 -0.905] [ 0.73 0. 0.866 1. -1. 0.5 0. 1. -0.5 -1. ] [-0.866 1. -0.866 -1. 1. -0.866 0. -1. 1. 0.866] [-0.408 -0.5 0.5 0.5 -0.866 1. -0.5 0.866 -1. -0.5 ] [ 0.44 -0.866 0.522 0. 0. -0.5 1. 0. 0.945 -0.577] [ 0.866 -1. 0.866 1. -1. 0.866 0. 1. -1. -0.866] [ 0.343 -0.189 0.189 -0.5 1. -1. 0.945 -1. 1. -0.189] [-0.762 0.866 -0.905 -1. 0.866 -0.5 -0.577 -0.866 -0.189 1. ]]
結果が正しいことを確認するために,元のデータの順番で相関行列を作成します.(このコードも必ずしも必要はありません.理解のためだけです.)この結果は前のページで求めたものと同じであることが確認できます.
raw_sim = np.zeros((algo.n_x, algo.n_x))
for i in range(algo.n_x):
  for j in range(algo.n_x):
    raw_sim[trainset.to_raw_uid(i), trainset.to_raw_uid(j)] = algo.sim[i, j]
print(raw_sim)
[[ 1. -1. -1. 0. 0.866 -1. 0.866 -0.866 0.866 1. ] [-1. 1. 1. -0.866 -0.5 -0.189 -0.5 0.866 0. 0. ] [-1. 1. 1. 0. -0.866 1. -0.866 0.866 -0.866 -1. ] [ 0. -0.866 0. 1. -0.5 0.945 0.522 -0.577 0.44 0. ] [ 0.866 -0.5 -0.866 -0.5 1. -1. 0.5 -0.5 -0.408 0.5 ] [-1. -0.189 1. 0.945 -1. 1. 0.189 -0.189 0.343 -0.5 ] [ 0.866 -0.5 -0.866 0.522 0.5 0.189 1. -0.905 0.966 0.866] [-0.866 0.866 0.866 -0.577 -0.5 -0.189 -0.905 1. -0.762 -1. ] [ 0.866 0. -0.866 0.44 -0.408 0.343 0.966 -0.762 1. 0.73 ] [ 1. 0. -1. 0. 0.5 -0.5 0.866 -1. 0.73 1. ]]
  モデルの当てはめができたので,Katoさん (id=2) の商品 D (id=3) についてのレイティングの予測値を求めます.予測値は est = 2.12 であることがわかりました.これも前のページで求めた結果と一致しました.
uid = 2
iid = 3
pred = algo.predict(uid, iid, verbose=False)
print(pred)
user: 2   item: 3   r_ui = None   est = 2.12   {'actual_k': 2, 'was_impossible': False}
モデルの中にもユーザ数やアイテム数の情報があることを確認しておきます.
print(f"Number of customers : {algo.n_x}")
print(f"Number of items     : {algo.n_y}")
Number of customers : 10 Number of items : 5
すべてのユーザ,アイテムの組み合わせについてモデルから得られる予測値を計算して行列に格納します.
predict_matrix = np.zeros((algo.n_x, algo.n_y))
for uid in range(algo.n_x):
    for iid in range(algo.n_y):
        pred = algo.predict(uid, iid, verbose=False)
        predict_matrix[uid, iid] = pred.est
print(predict_matrix)
print("---")
print(raw_matrix)
[[4.837 4.745 3.413 3.623 2.751] [1.982 2.337 3.686 2. 3.116] [2.151 3.417 3.936 2.125 3.238] [1.924 3.479 2.191 1.123 1.884] [4.236 3.447 2.87 3.75 2.75 ] [2.241 3.92 3.424 1.898 2.537] [3.366 3.06 2.29 1.685 1. ] [3.768 4.079 5. 3.75 4.884] [4.527 4.221 3.221 2.471 1.994] [3.087 2.95 1.738 1.792 1.062]] --- [[ 5. 4. 3. nan nan] [nan 2. 4. 2. 3.] [ 2. 3. 4. nan nan] [ 2. 3. 2. nan 2.] [ 4. 3. 3. 4. nan] [ 2. 5. nan 2. 3.] [ 3. 3. 2. nan 1.] [ 4. 4. 5. nan 5.] [ 5. 5. 4. 2. 1.] [ 3. nan 2. 2. 2.]]