Python入門トップページ


目次

  1. 協調フィルタリングによる推薦システム
  2. 顧客間の類似度に基づいた手法
  3. 個別データを順にマージしてみる
  4. 個別データを一気にマージしてみる
  5. 行列分解
  6. サンプルコードのまとめ
  7. Surprise による顧客間類似度に基づいた手法
  8. Surprise による行列分解
  9. ユーザごとの推薦アイテムリストを作成する
  10. 推薦アルゴリズムの性能を評価する
  11. 適合率と再現率
  12. クロスバリデーション
  13. 学習済みモデルの保存と読み込み

協調フィルタリングによる推薦システムを作ってみよう

学習済みモデルの保存と読み込み

このセクションで推薦システムを実験するために利用したデータはアイテムへの評価データが高々100件程度の小さなデータでした.このためモデルの学習を瞬時に終えることができていたことから,何度も学習を繰り返して実行することができていました.しかしながら,推薦システムを実際の現場のデータを用いて実験する際には数千万件や数億件のといった大きなデータを利用することになります.このような大きなデータでは学習のために多大な時間を要することから,一度学習した学習済みデータを保存しておき,この学習済みデータを用いて何らかの検証を行うことになるでしょう.

ここでは,学習済みモデルをファイルに保存する方法と,保存された学習済みモデルを読み込んで検証を行う方法を説明します.

目次に戻る

学習済みモデルの保存

まず,次のコードの37行目までは行列分解のモデルで学習を行なっており,先頭の import 文が異なるだけで,これまでの内容と同じです.学習済みモデルを保存するには,40行目のように,ファイル名「mf_model_trained.pkl」と,アルゴリズムに mf を指定して,dump.dump メソッドを呼び出すだけです.あるいは,46行目のように予測結果 predictions も併せて保存することも可能です.なお,24行目では biased = True を指定しているので,ユーザとアイテムのバイアスも利用するモデルを使っていることに注意してください.さらに,ここで利用しているデータは小さなデータですが,大きなデータを取り扱った場合,37行目の mf.fit() メソッドの実行に多大な時間を要することにも注意してください.


import pandas as pd
from surprise import Dataset, Reader, SVD, dump
from surprise.model_selection import train_test_split

# データの読み込み
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_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)

# 学習済みモデルを保存
dump.dump("mf_model_trained.pkl", algo=mf)

# テストデータに対しての予測を実行
predictions = mf.test(testset)

# 学習済みモデルと予測値を保存
dump.dump("mf_model_trained_predictions.pkl", algo=mf, predictions=predictions)

なお,mfpredictions にはどのような値が記録されているのかここで確認しておきます.


vars(mf)
{'n_factors': 5,
 'n_epochs': 100000,
 'biased': True,
 'init_mean': 0,
 'init_std_dev': 0.1,
 'lr_bu': 0.005,
 'lr_bi': 0.005,
 'lr_pu': 0.005,
 'lr_qi': 0.005,
 'reg_bu': 0.02,
 'reg_bi': 0.02,
 'reg_pu': 0.02,
 'reg_qi': 0.02,
 'random_state': None,
 'verbose': False,
 'bsl_options': {},
 'sim_options': {'user_based': True},
 'trainset': <surprise.trainset.Trainset at 0x10f792010>,
 'bu': array([ 0.19571111, -0.36765422, -0.31226492, -0.50712189, -0.07674016,
         0.42312658, -0.88529045,  0.41107763, -0.04832082,  0.87292237]),
 'bi': array([-0.374862  ,  0.50739329,  0.10106785,  0.21394463, -0.15570194,
        -0.34697589,  0.30967645, -0.10718365, -0.17492387, -0.22844063]),
 'pu': array([[-0.80395464,  0.71267944, -1.20498618, -0.40117738,  0.69148789],
        [ 0.19744055,  0.41997137, -0.45070174,  1.09691693, -0.41177925],
        [-0.35750156, -0.71053823,  0.17089033,  0.43654884,  1.04252597],
        [-0.24458814, -0.69607839, -0.26227029, -0.25759456, -0.0235782 ],
        [ 0.98079419,  0.41663715, -0.1080477 ,  0.59959773, -0.22561981],
        [-0.5236681 , -0.44396068,  0.56578997,  0.1320775 , -0.67196279],
        [ 0.24815216, -0.00910581,  0.12465436, -0.28681   ,  0.11562488],
        [-1.11542853, -0.16880383, -0.07616732, -0.28060989,  0.04457496],
        [ 1.08852736, -0.10461391,  0.01666894, -1.07851683,  0.24202194],
        [ 0.18793162,  1.09319727,  0.17101146,  0.25790805, -0.65114277]]),
 'qi': array([[-0.41231546, -0.03901252,  0.71038542,  0.03971459, -0.36211582],
        [ 0.20155429,  0.22947631, -0.35585227, -1.05450432,  0.53991937],
        [ 0.23550572,  0.7440862 , -0.53928394,  0.46153238, -0.23565508],
        [-1.15320449,  0.17806468, -0.364555  , -0.14405694, -0.039364  ],
        [ 0.56849154,  0.15686949,  0.59467644,  0.09381554, -1.24210839],
        [ 0.36001006,  0.86568662,  0.61900294,  0.80924388, -0.09959333],
        [-1.23550793,  0.07241156, -0.44911381,  0.81291952,  0.28431514],
        [ 0.24440808,  0.39483133,  0.38917093, -0.16405465, -1.09165998],
        [ 0.6105414 ,  0.57576394, -0.48698316, -0.45992765,  0.39326485],
        [ 0.17374784, -1.44111196,  0.72594273,  0.16038248,  0.30758894]])}

predictions
[Prediction(uid=0, iid=5, r_ui=2.0, est=3.1455116665210774, details={'was_impossible': False}),
 Prediction(uid=1, iid=5, r_ui=1.0, est=1.5550681175758898, details={'was_impossible': False}),
 Prediction(uid=2, iid=5, r_ui=1.0, est=2.213139666440644, details={'was_impossible': False}),
 Prediction(uid=0, iid=6, r_ui=4.0, est=2.932357746058268, details={'was_impossible': False}),
 Prediction(uid=1, iid=6, r_ui=3.0, est=2.833404636870037, details={'was_impossible': False}),
 Prediction(uid=2, iid=6, r_ui=3.0, est=3.326175903293922, details={'was_impossible': False}),
 Prediction(uid=3, iid=6, r_ui=2.0, est=2.0339223413220795, details={'was_impossible': False}),
 Prediction(uid=0, iid=7, r_ui=2.0, est=2.2377360169673657, details={'was_impossible': False}),
 Prediction(uid=1, iid=7, r_ui=4.0, est=3.3697121388396205, details={'was_impossible': False}),
 Prediction(uid=2, iid=7, r_ui=4.0, est=3.730866102461063, details={'was_impossible': False}),
 Prediction(uid=3, iid=7, r_ui=2.0, est=1.6827348658216295, details={'was_impossible': False}),
 Prediction(uid=0, iid=8, r_ui=3.0, est=2.6416295316564824, details={'was_impossible': False}),
 Prediction(uid=1, iid=8, r_ui=3.0, est=2.3728153298588084, details={'was_impossible': False}),
 Prediction(uid=2, iid=8, r_ui=3.0, est=3.275153567968708, details={'was_impossible': False}),
 Prediction(uid=3, iid=8, r_ui=2.0, est=2.2027285310156293, details={'was_impossible': False}),
 Prediction(uid=0, iid=9, r_ui=5.0, est=4.905419396217628, details={'was_impossible': False}),
 Prediction(uid=1, iid=9, r_ui=2.0, est=3.7055401375004293, details={'was_impossible': False}),
 Prediction(uid=2, iid=9, r_ui=2.0, est=2.523129923535726, details={'was_impossible': False}),
 Prediction(uid=3, iid=9, r_ui=1.0, est=1.860869148398716, details={'was_impossible': False})]

目次に戻る

学習済みモデルの読み込み

学習済みモデルを保存することができたので,別のプログラムから読み込んでみます.学習済モデルだけが保存された「md_model.trained.pkl」ファイルをロードするだけであれば,次のわずか2行で実現できます.


from surprise import dump
predictions, mf = dump.load("mf_model_trained.pkl")

読み込んだ学習済みモデルの中身を確認すると,保存時に確認した内容と同じであることがわかります.(つまり,長時間かけてもう一度学習し直す必要がないわけです.)


vars(mf)
{'n_factors': 5,
 'n_epochs': 100000,
 'biased': True,
 'init_mean': 0,
 'init_std_dev': 0.1,
 'lr_bu': 0.005,
 'lr_bi': 0.005,
 'lr_pu': 0.005,
 'lr_qi': 0.005,
 'reg_bu': 0.02,
 'reg_bi': 0.02,
 'reg_pu': 0.02,
 'reg_qi': 0.02,
 'random_state': None,
 'verbose': False,
 'bsl_options': {},
 'sim_options': {'user_based': True},
 'trainset': ,
 'bu': array([ 0.19571111, -0.36765422, -0.31226492, -0.50712189, -0.07674016,
         0.42312658, -0.88529045,  0.41107763, -0.04832082,  0.87292237]),
 'bi': array([-0.374862  ,  0.50739329,  0.10106785,  0.21394463, -0.15570194,
        -0.34697589,  0.30967645, -0.10718365, -0.17492387, -0.22844063]),
 'pu': array([[-0.43682477,  0.2374932 ,  0.071032  ,  1.56569262, -0.73599438],
        [ 0.4561568 , -0.24963529,  0.71511332, -0.33830809, -0.94606737],
        [ 1.02605794,  0.18019137, -0.81761928,  0.41940112,  0.09822586],
        [ 0.27151965, -0.09232856,  0.13209348,  0.44171509,  0.62067438],
        [ 0.13326573,  0.47296767,  0.56509041, -0.8152449 , -0.57639414],
        [ 0.12052911, -0.96323877, -0.10190252, -0.29368366,  0.47056956],
        [-0.17571753,  0.29045902, -0.10584042, -0.10208335,  0.18964152],
        [-0.09521895, -0.70418675, -0.34939065,  0.83340364,  0.193971  ],
        [-0.6199719 ,  1.1959955 ,  0.09253507, -0.25220804,  0.72865965],
        [-0.70090463, -0.31134677,  0.31994371, -0.62975949, -0.81423697]]),
 'qi': array([[-0.12856519, -0.69464026, -0.39239836, -0.34760634,  0.18713051],
        [-0.70979773,  0.82198072, -0.13606498,  0.5937992 ,  0.26987097],
        [-0.14494662,  0.07388613,  0.60353531, -0.08135203, -0.8781561 ],
        [-0.21086575, -0.7334811 , -0.11307399,  0.94055605, -0.19209267],
        [-0.42681379, -0.4798533 ,  0.55460829, -1.21822712,  0.22128824],
        [-0.05260312, -0.11204666, -0.22478085, -0.96113892, -0.96816512],
        [ 0.63792914, -0.82390503, -0.19379949,  0.89084167, -0.74710743],
        [-0.7050936 , -0.50460915,  0.46308253, -0.78110749,  0.09672035],
        [-0.52228407,  0.92945375,  0.21493273,  0.17395329, -0.3073881 ],
        [ 1.03923818,  0.04074622, -0.55578715, -0.3812895 ,  1.10379394]])}

予測値も含まれた「mf_model_trained_predictions.pkl」ファイルをロードします.


from surprise import dump
predictions, mf = dump.load("mf_model_trained_predictions.pkl")

学習済みモデルには上と同じ内容が読み込まれています(表示は省略します).予測は predictions に読み込まれているので,やはり保存時と同じ内容であることを確認します.


predictions
[Prediction(uid=0, iid=5, r_ui=2.0, est=3.1455116665415117, details={'was_impossible': False}),
 Prediction(uid=1, iid=5, r_ui=1.0, est=1.555068117484184, details={'was_impossible': False}),
 Prediction(uid=2, iid=5, r_ui=1.0, est=2.2131396664000276, details={'was_impossible': False}),
 Prediction(uid=0, iid=6, r_ui=4.0, est=2.9323577460371473, details={'was_impossible': False}),
 Prediction(uid=1, iid=6, r_ui=3.0, est=2.8334046368700427, details={'was_impossible': False}),
 Prediction(uid=2, iid=6, r_ui=3.0, est=3.3261759033207787, details={'was_impossible': False}),
 Prediction(uid=3, iid=6, r_ui=2.0, est=2.03392234132588, details={'was_impossible': False}),
 Prediction(uid=0, iid=7, r_ui=2.0, est=2.2377360168760005, details={'was_impossible': False}),
 Prediction(uid=1, iid=7, r_ui=4.0, est=3.3697121388548386, details={'was_impossible': False}),
 Prediction(uid=2, iid=7, r_ui=4.0, est=3.730866102586642, details={'was_impossible': False}),
 Prediction(uid=3, iid=7, r_ui=2.0, est=1.6827348658242072, details={'was_impossible': False}),
 Prediction(uid=0, iid=8, r_ui=3.0, est=2.6416295316471308, details={'was_impossible': False}),
 Prediction(uid=1, iid=8, r_ui=3.0, est=2.3728153298522607, details={'was_impossible': False}),
 Prediction(uid=2, iid=8, r_ui=3.0, est=3.2751535679892156, details={'was_impossible': False}),
 Prediction(uid=3, iid=8, r_ui=2.0, est=2.2027285310199547, details={'was_impossible': False}),
 Prediction(uid=0, iid=9, r_ui=5.0, est=4.9054193962123795, details={'was_impossible': False}),
 Prediction(uid=1, iid=9, r_ui=2.0, est=3.7055401375633807, details={'was_impossible': False}),
 Prediction(uid=2, iid=9, r_ui=2.0, est=2.5231299235396185, details={'was_impossible': False}),
 Prediction(uid=3, iid=9, r_ui=1.0, est=1.8608691484020063, details={'was_impossible': False})]

目次に戻る

学習済みモデルを読み込んで推薦アイテムリストを生成する

学習済みモデルを読み込む方法がわかったので,実際に読み込んでモデルの検証を行います.例えば同じテストデータを準備してユーザごとに推薦アイテムのリストを表示します.このとき,長時間かかるかもしれないモデルの学習 mf.fit をもう一度実行する必要がないことに注意してください.つまり,7行目で学習済みモデルを読み込み,準備したテストデータを利用して 39 行目で予測を実行できています.


import pandas as pd
from surprise import Dataset, Reader, dump
from surprise.model_selection import train_test_split
from collections import defaultdict

# 学習済みモデルのロード
predictions, mf = dump.load("mf_model_trained.pkl")

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)               # シャッフルあり

# テストデータに対しての予測を実行
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]}")
---- ユーザごとの推薦アイテム ----
ユーザ 0 への推薦アイテム: [9, 5, 6, 8, 7]
ユーザ 1 への推薦アイテム: [9, 7, 6, 8, 5]
ユーザ 2 への推薦アイテム: [7, 6, 8, 9, 5]
ユーザ 3 への推薦アイテム: [8, 6, 9, 7]

さらに,学習済みモデルと予測結果を読み込むと,データの準備や予測のステップまでもスキップして,読み込み後,直ちに推薦アイテムのリストを生成することができるようになります.


from surprise import dump
from collections import defaultdict

# 学習済みモデルのロード
predictions, mf = dump.load("mf_model_trained_predictions.pkl")

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

top_n = get_top_n(predictions, n=5)  # ここで抽出するアイテム数 n を設定する
print("---- ユーザごとの推薦アイテム ----")
for uid, user_ratings in top_n.items():
    print(f"ユーザ {uid} への推薦アイテム: {[iid for (iid, _) in user_ratings]}")
---- ユーザごとの推薦アイテム ----
ユーザ 0 への推薦アイテム: [9, 5, 6, 8, 7]
ユーザ 1 への推薦アイテム: [9, 7, 6, 8, 5]
ユーザ 2 への推薦アイテム: [7, 6, 8, 9, 5]
ユーザ 3 への推薦アイテム: [8, 6, 9, 7]

目次に戻る