Python入門トップページ


目次

  1. 協調フィルタリングによる推薦システム
  2. 顧客間の類似度に基づいた手法
  3. 個別データを順にマージしてみる
  4. 個別データを一気にマージしてみる
  5. 行列分解
  6. サンプルコードのまとめ

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

サンプルコードのまとめ

これまで示した断片的なコードをまとめて全体像を示します.なお,以前のページのコードにあった不要な箇所を削除したり,順序を入れ替えたりしています.

目次に戻る

顧客間の類似度に基づいた手法

import pandas as pd
import numpy as np

"""
パラメータの設定
↓↓↓↓↓ここから↓↓↓↓↓
"""

# データのURLを指定
url_items = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_items.csv?raw=true"
url_customers = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_customers.csv?raw=true"
url_ratings = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_ratings.csv?raw=true"

# どの顧客のどの商品について分析したいかを指定する
    # customer_id は 推定したい顧客(またはユーザ)
    # item_id は 推定したい商品(またはアイテム)
    # 次の4行のうち,いずれかの行を有効にする

# customer_id = 0; item_id = 3;
# customer_id= 0; item_id = 4;
customer_id= 2; item_id = 3;
# customer_id= 2; item_id = 4;

# 近傍顧客の数を設定する
n = 3

"""
パラメータの設定
↑↑↑↑↑ここまで↑↑↑↑↑
"""

def get_Y_il(y, i, l):
    """
    顧客 i と顧客 l の両方によって評価された評価の集合を返す
    """
    # まずは,yi と yl だけを取り出して2行の行列を生成
    yil = np.concatenate((y[i], y[l])).reshape(2,y.shape[1])
    # NaNが含まれる列のインデックスを初期化(あとでNaNが含まれている列は削除したい)
    nan_idx = []
    for i in range(yil.shape[1]):
        if np.isnan(yil[0][i]) or np.isnan(yil[1][i]):
            nan_idx.append(i)  # どちらかの顧客が未評価 (NaN) なら削除する列のリストに追加する
    # NaN が含まれる列を削除する
    return np.delete(yil, nan_idx, 1)

def sim_i_l(y, y_mean, i, l):
    """
    顧客 i と 顧客 l の類似度を返す関数
    """
    y_il = get_Y_il(y, i, l)
    term1 = np.sum((y_il[0] - y_mean[i]) * (y_il[1] - y_mean[l]))
    term2 = np.sqrt(np.sum((y_il[0] - y_mean[i])**2))
    term3 = np.sqrt(np.sum((y_il[1] - y_mean[l])**2))
    return term1 / term2 / term3

def get_sij(y, w, i, j):
    """
    顧客 i の 商品 j についてのスコアを返す関数
    """
    y_mean = np.nanmean(y, axis=1)
    y_mean_i = y_mean[i]
    # NaNが含まれている行,および自身の行は削除したい
    nan_idx = [i]
    for k in range(len(w)):
        if np.isnan(y[k][j]):
            nan_idx.append(k)
    y = np.delete(y, nan_idx, 0)
    y_mean = np.delete(y_mean, nan_idx)
    w = np.delete(w, nan_idx)
    # j 列目だけを取り出す
    y = y[:,j]
    sij = y_mean_i + np.sum(w * (y - y_mean)) / np.sum(w)
    return sij

"""
ここから実質的なメイン
"""
# CSVデータを読み込む
df_items = pd.read_csv(url_items)
df_customers = pd.read_csv(url_customers )
df_ratings = pd.read_csv(url_ratings)

# テーブルの結合
tbl = pd.merge(
    pd.merge(
        df_ratings, df_items,
        left_on = 'item',
        right_on = 'id',
    ),
    df_customers,
    left_on = 'customer', right_on = 'id'
)
# 表示系列名の変更
tbl = tbl.rename(columns={'item_y': 'item', 'id_y': 'id'})
# 列の選択と順序の変更
tbl = tbl.loc[:,['id', 'name','item','rating']]
# ピボットテーブルでデータを集計
df = tbl.pivot_table('rating', index=['id', 'name'], columns='item', aggfunc='sum')
# NumPy 配列に格納し,レイティング行列 y を生成する
y = df.values

"""
類似度の計算
"""
# 定式化での記号に合わせて内部的には i, と j を使う
i = customer_id
j = item_id
# y についてNaNを除いた平均を求める
y_mean = np.nanmean(y, axis=1)
# すべての顧客について,顧客 i との類似度を計算する.
sim = np.zeros(y.shape[0]) # 初期化
for k in range(0, sim.shape[0]):
    sim[k] = sim_i_l(y, y_mean, i, k)
# sim ベクトルの順位
customer_rank = np.argsort(np.argsort(sim)[::-1])
# (sim は 1 のはずの) 顧客 i 自身も含んだ近傍顧客のインデックスを取得する
idx = np.arange(len(sim))
near_customer_ids = idx[customer_rank <= n]
# 類似度 sim ベクトルから近傍顧客の類似度だけを取り出す
near_sim = sim[near_customer_ids]
# レイティング行列から近傍顧客のレイティングだけを取り出す
near_y = y[near_customer_ids]
# 変数 i が近傍顧客のみの集団での customer_id のインデックスとなるように更新する
i = np.where(near_customer_ids == i)[0][0]

"""
レイティングの推定
"""
sij = get_sij(near_y, near_sim, i, j)

"""
結果の表示
"""
print(df)
print(f"商品 {item_id} ({df.columns[item_id]}) に対する顧客 {customer_id } のスコアは {sij:5.3f} です.")
item        A    B    C    D    E
id name
0  Eto    5.0  4.0  3.0  NaN  NaN
1  Sato   NaN  2.0  4.0  2.0  3.0
2  Kato   2.0  3.0  4.0  NaN  NaN
3  Muto   2.0  3.0  2.0  NaN  2.0
4  Kito   4.0  3.0  3.0  4.0  NaN
5  Goto   2.0  5.0  NaN  2.0  3.0
6  Bito   3.0  3.0  2.0  NaN  1.0
7  Saito  4.0  4.0  5.0  NaN  5.0
8  Naito  5.0  5.0  4.0  2.0  1.0
9  Koto   3.0  NaN  2.0  2.0  2.0
商品 3 (D) に対する顧客 2 のスコアは 2.164 です.

なお,顧客 0 (Eto さん) と 顧客 2 (Kato さん) について商品 D, E のスコアをそれぞれ計算すると次のようになりました.これは上のコードの19行目から22行目のいずれか1行が有効になるように変更して実行するだけで結果が得られるはずです.この結果,Eto さんには商品 D を優先的に,Kato さんには商品 E を優先的に推薦すべきであることがわかりました.

item        A    B    C    D    E
id name
0  Eto    5.0  4.0  3.0  NaN  NaN
1  Sato   NaN  2.0  4.0  2.0  3.0
2  Kato   2.0  3.0  4.0  NaN  NaN
3  Muto   2.0  3.0  2.0  NaN  2.0
4  Kito   4.0  3.0  3.0  4.0  NaN
5  Goto   2.0  5.0  NaN  2.0  3.0
6  Bito   3.0  3.0  2.0  NaN  1.0
7  Saito  4.0  4.0  5.0  NaN  5.0
8  Naito  5.0  5.0  4.0  2.0  1.0
9  Koto   3.0  NaN  2.0  2.0  2.0
商品 3 (D) に対する顧客 0 のスコアは 4.108 です.
商品 4 (E) に対する顧客 0 のスコアは 3.330 です.
商品 3 (D) に対する顧客 2 のスコアは 2.164 です.
商品 4 (E) に対する顧客 2 のスコアは 3.294 です.

目次に戻る

行列分解

行列分解のコードもまとめておきます.なお,CSVファイルは行列形式ではなく個別のデータを読み込んでいます.また,前のページとは異なり,\(\lambda_1\) と \(\lambda_2\) には 0.001 という値を設定しています.この結果,前のページの結果よりもさらによい解が得られているように感じられます.具体的にはスコアで負の値や5を大幅に超える値がなくなっています.さらに「Eto さんには商品 D を優先的に,Kato さんには商品 E を優先的に推薦すべきである」という結果となり,これは顧客間の類似度に基づいた手法の結果と同じ結果になりました.

import pandas as pd
import numpy as np
import scipy.optimize as optimize

"""
パラメータの設定
↓↓↓↓↓ここから↓↓↓↓↓
"""
 # データのURLを指定
url_items = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_items.csv?raw=true"
url_customers = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_customers.csv?raw=true"
url_ratings = "https://github.com/rinsaka/sample-data-sets/blob/master/collaborative_filtering_ratings.csv?raw=true"

# 潜在因子数
L = 3
print("潜在因子数 L =", L)

# lambda1 と lambda2
lam1 = 1.0E-03
lam2 = 1.0E-03
print("lambda1, lambda2 = ", lam1, ", ", lam2)

"""
パラメータの設定
↑↑↑↑↑ここまで↑↑↑↑↑
"""

def get_L2rss_UV(UV, Y, M, N, L, lam1, lam2):
    """
    UとVを一つの一次元配列にした UV を受け取り,行列 U と V に変換して演算する
    """
    u, v = np.split(UV, [M*L])
    u = u.reshape(M, L)
    v = v.reshape(N, L)
    S = np.dot(u, v.T)
    # L2 ノルム
    ul2 = np.linalg.norm(u, ord=2)
    vl2 = np.linalg.norm(v, ord=2)
    return np.nansum((Y-S)**2 + lam1 * ul2 + lam2 * vl2)

"""
ここから実質的なメイン
"""
# CSVデータを読み込む
df_items = pd.read_csv(url_items)
df_customers = pd.read_csv(url_customers )
df_ratings = pd.read_csv(url_ratings)

# テーブルの結合
tbl = pd.merge(
    pd.merge(
        df_ratings, df_items,
        left_on = 'item',
        right_on = 'id',
    ),
    df_customers,
    left_on = 'customer', right_on = 'id'
)
# 表示系列名の変更
tbl = tbl.rename(columns={'item_y': 'item', 'id_y': 'id'})
# 列の選択と順序の変更
tbl = tbl.loc[:,['id', 'name','item','rating']]
# ピボットテーブルでデータを集計
df = tbl.pivot_table('rating', index=['id', 'name'], columns='item', aggfunc='sum')
# NumPy 配列に格納し,レイティング行列 y を生成する
y = df.values

M, N = y.shape
print("顧客数 M =", M)
print("アイテム数 N =",N)

# 乱数列の初期化
# rng = np.random.default_rng()
rng = np.random.default_rng(seed=1)

# -1, 1 の一様乱数
U = 2.0 * rng.random((M, L)) - 1.0
V = 2.0 * rng.random((N, L)) - 1.0
# 行列 U, V を一次元配列化して,連結する
UV = np.concatenate([U,V]).ravel()

results = optimize.minimize(
    get_L2rss_UV, UV, args=(y, M, N, L, lam1, lam2),
    method='Nelder-Mead',
    tol=0.001,
    options={'maxiter': 5000000}
)
print('    fun:', results.fun)
print('message:', results.message)
print('    status:', results.status)
print(' success:', results.success)
u, v = np.split(results.x, [M*L])
u = u.reshape(M, L)
v = v.reshape(N, L)
print('----- u -------')
print(u)
print('----- v -------')
print(v)
s = np.dot(u, v.T)
print('----- s -------')
print(s)
print('----- y -------')
print(y)
print('----- rss -------')
print(np.nansum((y-s)**2))
潜在因子数 L = 3
lambda1, lambda2 =  0.001 ,  0.001
顧客数 M = 10
アイテム数 N = 5
    fun: 6.6525765325362
message: Optimization terminated successfully.
 status: 0
success: True
----- u -------
[[-0.14384074  2.20955364  1.30961296]
 [ 0.4383522   2.604017    0.13283442]
 [ 0.35380988  2.99189426  0.36267718]
 [-1.25107984  5.09112913  0.16118577]
 [ 2.38424613 -2.77462355  1.64236359]
 [-1.09414837  6.63921267  0.15938274]
 [ 4.59723257 -8.50930598  2.27106171]
 [ 2.2808478   0.05055522  1.26055353]
 [-1.56966464  6.34459193  0.81931566]
 [ 0.17997507  1.80481305  0.70941435]]
----- v -------
[[-0.08764151  0.33713467  3.02470345]
 [ 0.58236121  0.64617267  2.30704094]
 [ 2.01846492  0.997201    0.49366814]
 [ 1.20677496  0.44781799  1.45912209]
 [ 2.60001363  0.97150142 -1.0464372 ]]
----- s -------
[[4.7187144  4.36531662 2.5595458  2.72677967 0.4021689 ]
 [1.2412713  2.24437838 3.547103   1.88893976 3.53052503]
 [2.07465378 2.97603656 3.87671494 2.29598327 3.44702115]
 [2.31358183 2.93303031 2.63119056 1.00531713 1.52454376]
 [3.82328209 3.38460659 2.85644238 4.03113117 1.78489137]
 [2.81628711 4.02059071 4.49081157 1.88532691 3.43841984]
 [3.59759775 2.41820132 1.91501508 5.05096115 1.30954105]
 [3.62994758 4.26909328 5.27652009 4.61441104 4.66025973]
 [4.75473658 5.07578487 3.56299043 2.14247202 1.22526824]
 [2.7384598  2.90767931 2.51325001 2.06053932 1.47895853]]
----- y -------
[[ 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.]]
----- rss -------
5.8802738100979655

目次に戻る


参考図書

  1. Dietmar Jannach, Markus Zanker, Alexander Felfernig, Gerhard Friedrich 著,田中克己,角谷和俊 監訳「情報推薦システム 入門:理論と実践」共立出版,2012年.
  2. Deepak K. Agarwal, Bee-Chung Chen 著,島田直希,大浦健志 訳「推薦システム:統計的機械学習の理論と実践」共立出版,2018年.

目次に戻る