これまでのページでは,データをおおよそ8対2に分割して8割のデータでモデルの学習(訓練)を行い,2割のデータで検証(バリデーション)を行なってきました.このとき,通常はデータをシャッフルしたあとにデータを分割することになります.
このページで考えるクロスバリデーションは,シャッフルしたデータをいくつかのグループに分割し,このうち1つのグループをバリデーションデータとして残し,その他のグループを訓練データとしてモデルを学習するという作業を,バリデーションデータのグループを変更しながら繰り返す作業をいいます.分割したグループの個数(このページでは4個)と同じ回数を繰り返してモデルの学習と性能評価を行い,その性能評価指標の平均値を取ることで,評価対象のモデルについてその汎化性能を測定することができるようになります.
まず,クロスバリデーションの仕組みを理解するために,データを4つのグループ分割して表示することだけを行なってみます.Surprise では surprise.model_selection.KFold
メソッドを使って簡単に K
個のクロスバリデーションデータを生成することができます.実際に89個の評価データを4分割し,一つひとつの塊がバリデーションデータ (testset) として取り扱えることを確認します.なお,17行目では乱数生成器の初期化で種を固定しているので,毎回同じシャッフルの結果になります.18行目を有効にするとシャッフルは行われません.19行目では毎回異なるランダムなシャッフルが行われます.さらに,22行目の繰り返しブロックで,訓練データ trainset
とバリデーションデータ testset
が取り出されており,その中ではバリデーションデータだけを表示しています.
import pandas as pd
import numpy as np
from surprise import Dataset, Reader
from surprise.model_selection import KFold
# データの読み込み
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)
# データ全体を n_splits 個に分けて,うち1個をバリデーションデータに,残りを学習データにする
n_splits = 4
kf = KFold(n_splits=n_splits, random_state=1) # 初期値を固定(毎回同じシャッフル)
# kf = KFold(n_splits=n_splits, shuffle=False) # シャッフルしない
# kf = KFold(n_splits=n_splits) # ランダムなシャッフル
# クロスバリデーション
for trainset, testset in kf.split(data):
print(f"number of trainset = {sum(len(r) for r in trainset.ur.values())}")
print(f"number of testset = {len(testset)}")
# バリデーションデータのマトリックス
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(test_raw_matrix)
print("----------------")
number of trainset = 66 number of testset = 23 [[ 5. nan nan nan nan 2. 4. 2. nan nan] [nan nan nan 2. nan nan nan nan nan nan] [ 2. nan nan nan nan nan nan nan 3. nan] [nan nan nan nan nan nan nan nan nan 1.] [nan 3. nan nan nan nan 4. 3. nan nan] [nan nan nan nan nan 3. 3. nan 4. nan] [nan 3. nan nan nan 4. nan nan 2. nan] [nan nan nan nan nan nan nan 5. nan nan] [nan nan 4. nan nan nan nan 2. nan 5.] [ 3. nan nan nan nan nan nan nan nan 3.]] ---------------- number of trainset = 67 number of testset = 22 [[nan nan nan nan nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan 2.] [nan nan nan nan nan nan nan nan nan 2.] [ 2. nan nan nan 2. 2. nan 2. 2. nan] [nan nan nan nan nan 4. nan nan nan 4.] [nan 5. nan nan nan nan nan nan nan nan] [ 3. nan nan nan nan nan 1. nan nan 4.] [ 4. nan nan nan 5. nan nan nan 4. 4.] [nan nan nan nan 1. 1. nan nan 4. nan] [nan nan nan nan 2. 3. nan nan nan nan]] ---------------- number of trainset = 67 number of testset = 22 [[nan nan 3. nan nan nan nan nan 3. 5.] [nan nan nan nan nan nan nan 4. 3. nan] [nan nan nan nan nan nan nan nan nan nan] [nan 3. 2. nan nan nan nan nan nan nan] [nan nan 3. nan nan nan nan nan 2. nan] [nan nan nan nan 3. nan nan 2. nan 1.] [nan nan 2. nan 1. nan nan 2. nan nan] [nan 4. nan nan nan 2. nan nan nan nan] [nan nan nan 2. nan nan 2. nan nan nan] [nan nan 2. nan nan nan 2. 1. nan nan]] ---------------- number of trainset = 67 number of testset = 22 [[nan 4. nan nan nan nan nan nan nan nan] [nan 2. 4. nan 3. 1. 3. nan nan nan] [nan 3. 4. nan nan 1. 3. 4. nan nan] [nan nan nan nan nan nan 2. nan nan nan] [ 4. nan nan 4. nan nan nan nan nan nan] [ 2. nan nan 2. nan nan nan nan nan nan] [nan nan nan nan nan nan nan nan nan nan] [nan nan 5. nan nan nan 5. nan nan nan] [ 5. 5. nan nan nan nan nan nan nan nan] [nan nan nan 2. nan nan nan nan 2. nan]] ----------------
上の実行結果をよく観察し,異なる訓練データとバリデーションデータの組み合わせが生成されていることを理解してください.
それでは,クロスバリデーションのためのデータ生成方法が理解できたところで,実際にモデルの学習と検証を行います.
import pandas as pd
import numpy as np
from surprise import Dataset, Reader, SVD
from surprise.model_selection import train_test_split
from surprise.model_selection import KFold
from collections import defaultdict
def precision_recall_at_k(predictions, k=10, threshold=3.5):
"""
ユーザごとの適合率と再現率を求めて返す
"""
# ユーザごとに {uid:[(予測評価値, 真の評価値),...], ...} のリストを作成する
user_est_true = defaultdict(list)
for uid, _, true_r, est, _ in predictions:
user_est_true[uid].append((est, true_r))
precisions = dict()
recalls = dict()
for uid, user_ratings in user_est_true.items():
# レイティングの予測値が高い順にソートする
user_ratings.sort(key=lambda x: x[0], reverse=True)
# 真の評価値が適合する(thresholdより高い)アイテムの数
n_rel = sum((true_r >= threshold) for (_, true_r) in user_ratings)
# 上位 k 個で予測評価値が適合する(thresholdより高い,つまり推薦される)アイテムの数
n_rec_k = sum((est >= threshold) for (est, _) in user_ratings[:k])
# 上位 k 個のうち推薦され,かつ適合もするアイテムの数
n_rel_and_rec_k = sum(
((true_r >= threshold) and (est >= threshold))
for (est, true_r) in user_ratings[:k]
)
# 適合率を計算する(ただし, 推薦アイテム数が0の場合は 適合率=0)
precisions[uid] = n_rel_and_rec_k / n_rec_k if n_rec_k != 0 else 0
# 再現率を計算する(ただし,適合アイテムが0の場合は 再現率=0)
recalls[uid] = n_rel_and_rec_k / n_rel if n_rel != 0 else 0
return precisions, recalls
# データの読み込み
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)
# 因子数
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
)
# データ全体を n_splits 個に分けて,うち1個をバリデーションデータに,残りを学習データにする
n_splits = 4
kf = KFold(n_splits=n_splits, random_state=1) # 初期値を固定(毎回同じシャッフル)
# kf = KFold(n_splits=n_splits, shuffle=False) # シャッフルしない
# kf = KFold(n_splits=n_splits) # ランダムなシャッフル
# クロスバリデーション
cross_precisions = []
cross_recalls = []
for trainset, testset in kf.split(data):
mf.fit(trainset)
predictions = mf.test(testset)
precisions, recalls = precision_recall_at_k(predictions, k=3, threshold=3)
cross_precisions.append(sum(prec for prec in precisions.values()) / len(precisions))
cross_recalls.append(sum(rec for rec in recalls.values()) / len(recalls))
print(f"Precisions = {cross_precisions}")
print(f"Recalls = {cross_recalls}")
print(f"Precision = {sum(cross_precisions)/len(cross_precisions):7.4f}")
print(f"Recall = {sum(cross_recalls)/len(cross_recalls):7.4f}")
Precisions = [0.4666666666666666, 0.2222222222222222, 0.4444444444444444, 0.4074074074074074] Recalls = [0.41666666666666663, 0.1388888888888889, 0.5, 0.2962962962962963] Precision = 0.3852 Recall = 0.3380
クロスバリデーションによってモデルの学習・検証が4度にわたって繰り返されています.そこで得られた適合度と再現率の平均値を最後に出力しています.