ここでは推薦アルゴリズムの性能を評価するためのいくつかの指標について理解し,簡単なデータについてそれらの指標を求めてみます.
まずは,プログラムの全体像を最初に示します.ハイライトした箇所が前のページのプログラムから追加した部分です.
import pandas as pd
import numpy as np
from surprise import Dataset, Reader, SVD
from surprise.model_selection import train_test_split
from surprise.accuracy import fcp, mae, mse, rmse
from collections import defaultdict
np.set_printoptions(precision=3) # 小数点以下の表示桁数を設定
np.set_printoptions(suppress=True) # 指数表示を行わないように
def get_top_n(predictions, n=10):
"""
ユーザごとにレイティングの予測値が高いN個の推薦リストを作成する
"""
# ユーザごとの予測値リストを作成
top_n = defaultdict(list)
for uid, iid, true_r, est, _ in predictions:
top_n[uid].append((iid, est))
# ソートして上位 n個のリストを作成する
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
# データの読み込み
url_ratings = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_ratings2.csv?raw=true"
df_ratings = pd.read_csv(url_ratings)
# レイティングのスケールを設定する
reader = Reader(rating_scale=(1, 5))
# 列名を指定してデータフレームからデータセットを読み込む
data = Dataset.load_from_df(df_ratings[["customer", "item", "rating"]], reader)
# 19件をテストデータにする
trainset, testset = train_test_split(data, test_size=0.21, shuffle=False) # シャッフルなし
# trainset, testset = train_test_split(data, test_size=0.21) # シャッフルあり
# 学習データの数をカウントする
n_train = sum(len(d) for d in trainset.ur.values())
# データ数などを確認する
print(f"number of trainset customers ={trainset.n_users:4d}")
print(f"number of trainset items ={trainset.n_items:4d}")
print(f"number of elements in matrix ={trainset.n_users * trainset.n_items:4d}")
print(f"number of null ratings ={trainset.n_users * trainset.n_items - df_ratings['rating'].shape[0]:4d}")
print(f"number of ratings in data ={df_ratings['rating'].shape[0]:4d}")
print(f"number of ratings in trainset ={n_train:4d}")
print(f"number of ratings in testset ={len(testset):4d}")
# 学習データを行列形式に変換し,元の順番に並べ替えた行列も作成する
trainset_matrix = np.zeros((trainset.n_users, trainset.n_items))
train_raw_matrix = np.zeros((trainset.n_users, trainset.n_items)) # 元のデータ
trainset_matrix[:,:] = np.nan
train_raw_matrix[:,:] = np.nan
for u, vals in trainset.ur.items():
for i, rating in vals:
trainset_matrix[u, i] = rating
train_raw_matrix[trainset.to_raw_uid(u), trainset.to_raw_iid(i)] = rating
# 因子数
n_factors = 5
# エポック数
n_epochs = 100000
# バイアスを利用するか
biased = False
# biased = True
# 学習率
lr_all = 0.005
# 正則化パラメータ
reg_all = 0.02
# モデルの設定
mf = SVD(n_factors=n_factors,
n_epochs=n_epochs,
biased=biased,
lr_all=lr_all,
reg_all=reg_all
)
# モデルの当てはめ(学習)を行う
mf.fit(trainset)
# 予測評価値のマトリックス
predict_matrix = np.zeros((trainset.n_users, trainset.n_items))
for uid in range(trainset.n_users):
for iid in range(trainset.n_items):
pred = mf.predict(uid, iid, verbose=False)
predict_matrix[uid, iid] = pred.est
# テストデータのマトリックス
test_raw_matrix = np.zeros((trainset.n_users, trainset.n_items))
test_raw_matrix[:] = np.nan
for r, c, v in testset:
test_raw_matrix[r, c] = v
print("----- trainset -------")
print(train_raw_matrix)
print("----- testset -----")
print(test_raw_matrix)
print("----- s -------")
print(predict_matrix)
print("----- train_predict -------")
train_predict_matrix = np.copy(train_raw_matrix)
train_predict_matrix = np.where(~np.isnan(train_predict_matrix), predict_matrix, np.nan)
print(train_predict_matrix)
print("----- test_predict -------")
test_predict_matrix = np.copy(test_raw_matrix)
test_predict_matrix = np.where(~np.isnan(test_predict_matrix), predict_matrix, np.nan)
print(test_predict_matrix)
# 学習データに対する予測
train_predictions = [mf.predict(trainset._inner2raw_id_users[uid], trainset._inner2raw_id_items[iid], r_ui) for (uid, iid, r_ui) in trainset.all_ratings()]
# テストデータに対しての予測を実行
test_predictions = mf.test(testset)
top_n = get_top_n(test_predictions, n=5) # ここで抽出するアイテム数 n を設定する
print("---- ユーザごとの推薦アイテム ----")
for uid, user_ratings in top_n.items():
print(f"ユーザ {uid} への推薦アイテム: {[iid for (iid, _) in user_ratings]}")
print("----- FCP (Fraction of Concordant Pairs) -----")
train_fcp = fcp(train_predictions, verbose=False)
print(f"train_fcp = {train_fcp:8.5f}")
test_fcp = fcp(test_predictions, verbose=False)
print(f"test_fcp = {test_fcp:8.5f}")
print("----- MAE (Mean Absolute Error) -----")
train_mae = mae(train_predictions, verbose=False)
print(f"train_mae = {train_mae:8.5f}")
test_mae = mae(test_predictions, verbose=False)
print(f"test_mae = {test_mae:8.5f}")
print("----- MSE (Mean Squared Error) -----")
train_mse = mse(train_predictions, verbose=False)
print(f"train_mse = {train_mse:8.5f}")
test_mse = mse(test_predictions, verbose=False)
print(f"test_mse = {test_mse:8.5f}")
print("----- RMSE (Root Mean Squared Error) -----")
train_rmse = rmse(train_predictions, verbose=False)
print(f"train_rmse = {train_rmse:8.5f}")
test_rmse = rmse(test_predictions, verbose=False)
print(f"test_rmse = {test_rmse:8.5f}")
number of trainset customers = 10 number of trainset items = 10 number of elements in matrix = 100 number of null ratings = 11 number of ratings in data = 89 number of ratings in trainset = 70 number of ratings in testset = 19 ----- trainset ------- [[ 5. 4. 3. nan nan nan nan nan nan nan] [nan 2. 4. 2. 3. nan nan nan nan nan] [ 2. 3. 4. nan nan nan nan nan nan nan] [ 2. 3. 2. nan 2. 2. nan nan nan nan] [ 4. 3. 3. 4. nan 4. 4. 3. 2. 4.] [ 2. 5. nan 2. 3. 3. 3. 2. 4. 1.] [ 3. 3. 2. nan 1. 4. 1. 2. 2. 4.] [ 4. 4. 5. nan 5. 2. 5. 5. 4. 4.] [ 5. 5. 4. 2. 1. 1. 2. 2. 4. 5.] [ 3. nan 2. 2. 2. 3. 2. 1. 2. 3.]] ----- testset ----- [[nan nan nan nan nan 2. 4. 2. 3. 5.] [nan nan nan nan nan 1. 3. 4. 3. 2.] [nan nan nan nan nan 1. 3. 4. 3. 2.] [nan nan nan nan nan nan 2. 2. 2. 1.] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan]] ----- s ------- [[4.937 3.993 3.01 2.736 1.836 2.556 2.369 1.595 2.642 5. ] [2.155 2.037 3.891 2.033 2.976 1. 3.113 4.036 2.813 2.374] [2.026 3. 3.919 1.806 2.796 1. 2.955 3.77 3.48 2.029] [2.001 2.959 2.004 1.643 1.977 1.994 2.028 1.412 2.273 1.642] [3.934 3.015 2.981 3.876 4.201 3.947 3.956 2.987 2.046 4.02 ] [2.01 4.895 2.9 1.973 3.001 2.956 2.98 1.991 3.941 1.084] [2.999 2.985 2.009 1.874 1.048 3.901 1.049 1.929 1.984 3.945] [3.993 3.985 5. 3.812 4.921 2.042 4.946 4.908 3.955 3.95 ] [4.909 4.915 3.967 1.958 1.077 1.062 2.007 2.002 3.962 4.958] [3.023 3.182 1.901 2.132 1.877 2.932 1.982 1.137 2.005 2.896]] ----- train_predict ------- [[4.937 3.993 3.01 nan nan nan nan nan nan nan] [ nan 2.037 3.891 2.033 2.976 nan nan nan nan nan] [2.026 3. 3.919 nan nan nan nan nan nan nan] [2.001 2.959 2.004 nan 1.977 1.994 nan nan nan nan] [3.934 3.015 2.981 3.876 nan 3.947 3.956 2.987 2.046 4.02 ] [2.01 4.895 nan 1.973 3.001 2.956 2.98 1.991 3.941 1.084] [2.999 2.985 2.009 nan 1.048 3.901 1.049 1.929 1.984 3.945] [3.993 3.985 5. nan 4.921 2.042 4.946 4.908 3.955 3.95 ] [4.909 4.915 3.967 1.958 1.077 1.062 2.007 2.002 3.962 4.958] [3.023 nan 1.901 2.132 1.877 2.932 1.982 1.137 2.005 2.896]] ----- test_predict ------- [[ nan nan nan nan nan 2.556 2.369 1.595 2.642 5. ] [ nan nan nan nan nan 1. 3.113 4.036 2.813 2.374] [ nan nan nan nan nan 1. 2.955 3.77 3.48 2.029] [ nan nan nan nan nan nan 2.028 1.412 2.273 1.642] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan] [ nan nan nan nan nan nan nan nan nan nan]] ---- ユーザごとの推薦アイテム ---- ユーザ 0 への推薦アイテム: [9, 8, 5, 6, 7] ユーザ 1 への推薦アイテム: [7, 6, 8, 9, 5] ユーザ 2 への推薦アイテム: [7, 8, 6, 9, 5] ユーザ 3 への推薦アイテム: [8, 6, 9, 7] ----- FCP (Fraction of Concordant Pairs) ----- train_fcp = 1.00000 test_fcp = 0.81818 ----- MAE (Mean Absolute Error) ----- train_mae = 0.04510 test_mae = 0.31444 ----- MSE (Mean Squared Error) ----- train_mse = 0.00339 test_mse = 0.24051 ----- RMSE (Root Mean Squared Error) ----- train_rmse = 0.05819 test_rmse = 0.49042
FCP,MAE,MSE,RMSEのそれぞれについて順番に意味を確認しましょう.
FCP (Fraction of Concordant Pairs) は推薦アルゴリズムの評価指標の一つで,ユーザの好みをどれだけ正確に予測できているかを測定します.具体的には,ユーザの好みと予測結果の好みの順序がどの程度一致しているかを評価します.Surprise ライブラリでは surprise.accuracy.fcp()
を利用することで簡単に求めることができます.
train_fcp = fcp(train_predictions, verbose=False)
print(f"train_fcp = {train_fcp:8.5f}")
test_fcp = fcp(test_predictions, verbose=False)
print(f"test_fcp = {test_fcp:8.5f}")
----- FCP (Fraction of Concordant Pairs) ----- train_fcp = 1.00000 test_fcp = 0.81818
FCP の定義は次の通りです.
ここで,\(n_c\) は感覚的には予測値の大小関係と実際の大小関係が正しかったデータ数(正解数)で,\(n_d\) は大小関係に誤りがあった数(不正解数)です.詳細は Koren and Sill による論文の式(7),式(8) を参照してください.また,ソースコードはこちらを参照して下さい.
テストデータについての FCP が,test_fcp = 0.81818
となることを実際に確かめます.まず,テストデータの行列(つまり正解データ)について,値が存在する右上の箇所のみを表示し,予測値についても同じ箇所を表示します.
print("----- testset -----")
print(test_raw_matrix[:4,5:])
print("----- test_predict -------")
print(test_predict_matrix[:4,5:])
----- testset ----- [[ 2. 4. 2. 3. 5.] [ 1. 3. 4. 3. 2.] [ 1. 3. 4. 3. 2.] [nan 2. 2. 2. 1.]] ----- test_predict ------- [[2.556 2.369 1.595 2.642 5. ] [1. 3.113 4.036 2.813 2.374] [1. 2.955 3.77 3.48 2.029] [ nan 2.028 1.412 2.273 1.642]]
さらに,それぞれの先頭行のみ(つまりユーザ0のみ)を表示します.
print(test_raw_matrix[:1,5:])
print(test_predict_matrix[:1,5:])
[[2. 4. 2. 3. 5. ]] [[2.556 2.369 1.595 2.642 5. ]]
この値についての大小比較を行います.まず,ユーザ0 に対して \(n_c^0 = 0\),\(n_d^0 = 0\) で初期化した後,最初のアイテムである先頭データの予測値 2.556
について着目します.予測値がこの 2.556
よりも小さいアイテムを探します.
[[2. 4. 2. 3. 5. ]] [[2.556 2.369 1.595 2.642 5. ]]
すると,2.369
と 1.595
の2個のアイテムが見つかりました.この2つのアイテムに対する正解データについて,基準となるアイテムの正解データである2より小さな値はないので \(n_c^0 = 0\) のままで,大きな値(4)が1つあるので,\(n_d^0 = 1\) になります.これはつまり,2つのアイテムに関して正解データの大小関係と予測値の大小関係が逆転している(予測に失敗している)ことを意味しています.
[[2. 4. 2. 3. 5. ]] [[2.556 2.369 1.595 2.642 5. ]]
次は,2番目のアイテムについて着目します.予測値が 2.369
より小さいアイテムを探すと 1.595
のアイテムがあり,そのアイテムの正解データ (2) は基準となる正解データ (4) よりも小さい(つまり,順序が正しい)ことから \(n_c^0\) に 1 加えて \(n_c^0 = 1\) となります.
[[2. 4. 2. 3. 5. ]] [[2.556 2.369 1.595 2.642 5. ]]
3番目のアイテムについては予測値がそれよりも小さいものがないのでスキップします.
[[2. 4. 2. 3. 5. ]]
[[2.556 2.369 1.595 2.642 5. ]]
次は4番目のアイテムを基準として,その基準となる予測値 2.642
よりも予測値が小さいアイテムが3つあることを確認します.基準となるアイテムの正解データ (3) よりも正解データが小さいアイテム(つまり順序が正しいもの)は2つあるので,\(n_c^0\) に 2 を加えて \(n_c^0 = 3\) となります.反対に基準となるアイテムの正解データ (3) よりも正解データが大きいアイテム(つまり順序が反対であるもの)は1つあるので,\(n_d^0\) に 1 を加えて \(n_d^0 = 2\) となります.
[[2. 4. 2. 3. 5. ]] [[2.556 2.369 1.595 2.642 5. ]]
最後のアイテムを基準アイテムとします.基準となる予測値 (5) よりも他の4つのアイテムの予測値が小さく,かつ正解データも小さい(つまり順序が正しい)ことから \(n_c^0\) に 4 を加えて \(n_c^0 = 7\) となりました.
[[2. 4. 2. 3. 5. ]] [[2.556 2.369 1.595 2.642 5. ]]
したがって,ユーザ0に対して,\(n_c^0 = 7\),\(n_d^0 = 2\) となりました.同じ様に残りの3名のユーザについても計算すると,次のような結果になります.
n_c^u = [7, 9, 9, 2] n_d^u = [2, 0, 0, 1]
それぞれの合計を求めると次の結果になりました.
したがって,FCP は次の通り 0.9 となるはずです.
Koren and Sill による論文の式(7),式(8) のとおり計算すると上で求めた結果 FCP = 0.9 になるはずです.しかしながら,surprise のソースコード を確認すると次のように nc
と nd
を合計値ではなく,平均値として計算しています.
Source code for surprise.accuracy
nc = np.mean(list(nc_u.values())) if nc_u else 0
nd = np.mean(list(nd_u.values())) if nd_u else 0
この結果,値が0であったアイテムが平均の計算で分母から除外されてしまっています.
したがって,surprise の fcp
でを計算すると 0.81818 となります.
Surprise では意図してそのような書き方をしたのか,バグであるのかは確認できていません.情報があれば教えて下さい.surprise のソースコードを次のように変更すると,期待通り 0.9 が求められます.
nc = np.sum(list(nc_u.values())) if nc_u else 0
nd = np.sum(list(nd_u.values())) if nd_u else 0
いずれにせよ,\(n_c\) は正解データと予測における評価値の大小関係が正しく求められた回数であり,\(n_d\) は大小関係で誤った推定を行なった回数を意味します.このことから直前の式を見ても分かる通り,FCPは0から1の間の値をとり,1に近いほどアイテムの好みについてその大小関係を正しく予測できていることを意味します.
MAE(Mean Absolute Error:平均絶対誤差)の定義は次の通りです.
Surprise では 訓練データやテストデータを mae
関数に渡すだけで,MAE を簡単に求めることができます.
train_mae = mae(train_predictions, verbose=False)
print(f"train_mae = {train_mae:8.5f}")
test_mae = mae(test_predictions, verbose=False)
print(f"test_mae = {test_mae:8.5f}")
train_mae = 0.04510 test_mae = 0.31444
なお,これを式の定義通りに自力で求めることも可能です.
print(f"train_mae = {np.nansum(np.abs(train_raw_matrix-predict_matrix))/np.count_nonzero(~np.isnan(train_raw_matrix)):8.5f}")
print(f"test_mae = {np.nansum(np.abs(test_raw_matrix-predict_matrix))/np.count_nonzero(~np.isnan(test_raw_matrix)):8.5f}")
train_mae = 0.04510 test_mae = 0.31444
MSE (Mean Squared Error:平均二乗誤差) の定義は次の通りです.
train_mse = mse(train_predictions, verbose=False)
print(f"train_mse = {train_mse:8.5f}")
test_mse = mse(test_predictions, verbose=False)
print(f"test_mse = {test_mse:8.5f}")
train_mse = 0.00339 test_mse = 0.24051
やはり,定義通りに求めても同じ結果が得られるはずです.
print(f"train_mse = {np.nansum((train_raw_matrix-predict_matrix)**2)/np.count_nonzero(~np.isnan(train_raw_matrix)):8.5f}")
print(f"test_mse = {np.nansum((test_raw_matrix-predict_matrix)**2)/np.count_nonzero(~np.isnan(test_raw_matrix)):8.5f}")
train_mse = 0.00339 test_mse = 0.24051
RMSE (Root Mean Squared Error:二乗平均平方根誤差) の定義は次の通りで,MSE の平方根です.
train_rmse = rmse(train_predictions, verbose=False)
print(f"train_rmse = {train_rmse:8.5f}")
test_rmse = rmse(test_predictions, verbose=False)
print(f"test_rmse = {test_rmse:8.5f}")
train_rmse = 0.05819 test_rmse = 0.49042
もちろん定義通りに求めることができるはずです.
print(f"train_rmse = {np.sqrt(np.nansum((train_raw_matrix-predict_matrix)**2)/np.count_nonzero(~np.isnan(train_raw_matrix))):8.5f}")
print(f"test_rmse = {np.sqrt(np.nansum((test_raw_matrix-predict_matrix)**2)/np.count_nonzero(~np.isnan(test_raw_matrix))):8.5f}")
train_rmse = 0.05819 test_rmse = 0.49042
さらにすでに求めていた平均二乗誤差から平方根を計算してもよいでしょう.
print(f"train_rmse = {np.sqrt(train_mse):8.5f}")
print(f"test_rmse = {np.sqrt(test_mse):8.5f}")
train_rmse = 0.05819 test_rmse = 0.49042