import numpy as np
np.nan
を使う。np.array
を使う。Du_list = [[5, 3, 1],
[6, 2, 1],
[4, 1, 1],
[8, 5, -1],
[2, 4, -1],
[3, 6, -1],
[7, 6, -1],
[4, 2, np.nan],
[5, 1, np.nan],
[8, 6, np.nan],
[3, 4, np.nan],
[4, 7, np.nan],
[4, 4, np.nan]]
Du = np.array(Du_list)
print(f'Du = \n{Du}')
np.shape
を使う。print(f'Duの形状 = {Du.shape}')
np.shape
を使う。print(f'Duの行数 = {Du.shape[0]}')
np.shape
を使う。print(f'Duの列数 = {Du.shape[1]}')
np.size
を使う。
np.shape[0]*np.shape[1]
でもよい。print(f'Duの全要素数 = {Du.size}')
I = np.arange(Du.shape[0])
print(f'I = {I}')
x = Du[:, :-1]
print(f'x = \n{x}')
i = 0
print(f'x{i} = {x[i]}')
ru = Du[:, -1]
print(f'ru = {ru}')
print(f'ruの形状 = {ru.shape}')
print(f'ruの全要素数 = {ru.size}')
i = 2
j = 5
print(f'ru{i}からru{j-1}までの評価値 = {ru[i:j]}')
ru[::-1]
により、逆順で値を取り出す。print(f'ruの逆順 = {ru[::-1]}')
ru[i]
を使う。i = 0
print(f'ru{i} = {ru[i]}')
np.nan
であるときユーザーがアイテムを未評価であるとして問題を解く。np.isnan(nparray)
を使う。print(f'ユーザuが未評価 = {np.isnan(ru)}')
np.isnan(nparray)
に対して、~
をつけてあげればTrue
とFalse
が反転するので題意を満たす。print(f'ユーザuが評価済み = {~np.isnan(ru)}')
np.arange()
について、16の結果を使ってインデキシングすることで解決する。Iu = np.arange(ru.shape[0])[~np.isnan(ru)]
print(f'Iu = {Iu}')
np.where(ruの条件, True, False)
を使う。Iuplus = np.arange(ru.shape[0])[np.where(ru == 1, True, False)]
print(f'Iu+ = {Iuplus}')
np.where()
の条件部分をいじればよい。Iuminus = np.arange(ru.shape[0])[np.where(ru == -1, True, False)]
print(f'Iu- = {Iuminus}')
np.nan
かどうかでインデキシングする。numpy.setdiff1d()
については、また今度。Iu_not = np.arange(ru.shape[0])[np.isnan(ru)]
print(f'Iu_not = {Iu_not}')
Du
に直接適用すればできるはず。DuL = Du[~np.isnan(ru)]
print(f'DuL = \n{DuL}')
np.shape
でDuL
の行数を求めればよい。print(f'|DuL| = {DuL.shape[0]}')
np.where(ruの条件, True, False)
の長さは13なので、これでインデキシングしたい場合DuL
を対象としてもエラーが発生する。(一敗)DuL[:, -1]
、つまりDuL
の中でも評価値の部分に対してnp.where
を利用した。print(f'|DuL+| = {DuL[np.where(DuL[:, -1] == 1, True, False)].shape[0]}')
print(f'|DuL-| = {DuL[np.where(DuL[:, -1] == -1, True, False)].shape[0]}')
~
を外すだけ。DuU = Du[np.isnan(ru)]
print(f'DuU = \n{DuU}')
DuU
として、22と同じことを行う。print(f'|DuU| = {DuU.shape[0]}')
import pprint
import numpy as np
np.set_printoptions(precision=3)
np.nan
とnp.array
を使う。R_list = [[np.nan, 4, 3, 1, 2, np.nan],
[5, 5, 4, np.nan, 3, 3],
[4, np.nan, 5, 3, 2, np.nan],
[np.nan, 3, np.nan, 2, 1, 1],
[2, 1, 2, 4, np.nan, 3]]
R = np.array(R_list)
print(f'R = \n{R}')
np.arange(np.shape[0])
を使う。U = np.arange(R.shape[0])
print(f'U = {U}')
np.arange(np.shape[1])
でなんとかする。I = np.arange(R.shape[1])
print(f'I = {I}')
U
に対してnp.size
を使うらしい。行列ではなくベクトルの場合だとnp.shape
との違いがいまいちわからなかったりするので調べておこうと思う。print(f'|U| = {U.size}')
print(f'|I| = {I.size}')
u = 0
i = 1
print(f'r{u}{i} = {R[u, i]}')
np.size
を使っていた。今回は、np.shape[0]*np.shape[1]
で値を取り出してみる。print(f'Rの全要素数 = {R.shape[0]*R.shape[1]}')
np.isnan(R)
によりR
の各成分が欠損値かどうかを確認する。今回の場合は観測されているならばTrue
とするようなので別途~
を付け加える必要がある。print(f'観測値 = \n{~np.isnan(R)}')
np.count.nonzero()
を使えばよいらしい。そのまま真偽値のみで構成された行列に適用すると、True
の数を数えてくれるようだ。print(f'|R| = {np.count_nonzero(~np.isnan(R))}')
R.shape[index]
ではなくR.size
を使って計算している。sparsity = (R.size-np.count_nonzero(~np.isnan(R)))/R.size
print(f'sparsity = {sparsity:.3f}')
R
のu
行目の中で欠損値ではないインデックスのみを返すようにする。u = 0
print(f'I{u} = {I[~np.isnan(R[u, :])]}')
ndarray
のリストとしてまとめるという点
Iu = [I[~np.isnan(R[u, :])] for u in range(U.size)]
print('Iu = ')
pprint.pprint(Iu)
np.intersect1d(ndarray1, ndarray2)
を使えばよい。u = 0
v = 1
temp_u = Iu[u]
temp_v = Iu[v]
Iuv = np.intersect1d(temp_u, temp_v)
print(f'I{u}{v} = {Iuv}')
U
から列目がnp.nan
でないインデックスを返すように~np.isnan()
を使う。i = 0
print(f'U{i} = {U[~np.isnan(R[:, i])]}')
Ui = [U[~np.isnan(R[:, i])] for i in range(I.size)]
print('Ui = ')
pprint.pprint(Ui)
i = 0
j = 4
temp_i = Ui[i]
temp_j = Ui[j]
Uij = np.intersect1d(temp_i, temp_j)
print(f'U{i}{j} = {Uij}')
R
をnp.mean()
で平均することを考えたい。すると、悲しいことに以下のようになるのでnp.nan
を避ける必要がある。np.mean(R)
np.nanmean()
をすることでnp.nan
を除外した平均を算出できる。print(f'R全体の平均評価値 = {np.nanmean(R):.3f}')
np.nanmean()
もnp.mean()
と同様にaxis
を設定できるのでそれを素直に行うことで結果を得る。この場合axis=0
を指定する必要がある。ri_mean = np.nanmean(R, axis=0)
print(f'ri_mean = {ri_mean}')
axis=1
版を作ればよい。ru_mean = np.nanmean(R, axis=1)
print(f'ru_mean = {ru_mean}')
ndarray.reshape()
を使うことで望みの結果を得る。今回はru_mean
を縦ベクトルのような表示にすることが目標となっていて、reshape(-1, 1)
とするのがいいらしいが紛らわしいと思ってしまった。print(f'ru_mean = \n{ru_mean.reshape(-1, 1)}')
R - ru_mean
は計算できず、エラーが発生する。ブロードキャストによって計算できそうだと期待してやってみた結果これがでたのでちょっとつらいが内包表記
によってR
との差をとるような計算を試みた。理想的な解答とはかなり乖離していると思われるので、あとで調べてメモっておく。ndarray.reshape()
を使う方法を思いつかなかったのが悲しい。rubar = np.array([[rm] * I.size for rm in ru_mean])
R2 = R - rubar
print(f'R\' = \n{R2}')
ru_mean
ではなくru_mean.reshape(-1, 1)
を使えばR
との差が直接計算できるということに後で気づいたのでコードを次のセルに記載する。R2 = R - ru_mean.reshape(-1, 1)
print(f'R\' = \n{R2}')
import pprint
import numpy as np
# 上位K件
TOP_K = 3
Du = np.array([
[5, 3, +1],
[6, 2, +1],
[4, 1, +1],
[8, 5, -1],
[2, 4, -1],
[3, 6, -1],
[7, 6, -1],
[4, 2, np.nan],
[5, 1, np.nan],
[8, 6, np.nan],
[3, 4, np.nan],
[4, 7, np.nan],
[4, 4, np.nan],
])
I = np.arange(Du.shape[0])
x = Du[:,:-1]
ru = Du[:,-1]
Iu = I[~np.isnan(ru)]
Iup = I[ru==+1]
Iun = I[ru==-1]
Iu_not = np.setdiff1d(I, Iu)
x
は以下の通りであるから、ru
が1
となっているインデックスのみがTrue
になるようなベクトルを使ってあげればよいはず。x
np.where()
を用いることによって、ru
の値が1
のところのみTrue
になるようにしている。print(f'x[Iu+] = \n{x[np.where(ru == 1, True, False)]}')
Iu+
に対して、axis
を工夫して総和をとればよいはずだ。今回は行方向に和をとるはずだから、axis=0
を使えばいいかもしれない。print(f'sum(x[Iu+]) = {x[np.where(ru == 1, True, False)].sum(axis=0)}')
pu = x[np.where(ru == 1, True, False)].mean(axis=0)
print(f'pu = {pu}')
cos
を完成させることを考える。num
は、よく用いられる@
かnp.dot()
を利用して計算されるので今回もそれを用いて計算していく。den_u
は、np.linalg.norm()
をユーザプロファイルのベクトルに使うことで計算可能なはずだ。引数ord
は何も設定しない場合デフォルトで2
になったような気もするが、一応指定はしておく。np.linalg.norm()
の引数に指定するベクトルをxi
に変更すればよい。cos
関数を作成し、実行結果を確認している。def cos(pu, xi):
"""
コサイン類似度関数:ユーザプロファイルpuとアイテムiの特徴ベクトルxiのコサイン類似度を返す。
Parameters
----------
pu : ndarray
ユーザuのユーザプロファイル
xi : ndarray
アイテムiの特徴ベクトル
Returns
-------
float
コサイン類似度
"""
num = pu@xi
print(f'num = {num}')
den_u = np.linalg.norm(pu, ord=2)
print('den_u = {:.3f}'.format(den_u))
den_i = np.linalg.norm(xi, ord=2)
print('den_i = {:.3f}'.format(den_i))
cosine = num / (den_u * den_i)
return cosine
u = 0
i = 7
print(f'cos(p{u}, x{i}) = {cos(pu, x[i]):.3f}')
u = 0
i = 11
print(f'cos(p{u}, x{i}) = {cos(pu, x[i]):.3f}')
order
を完成させることを考える。scores
は、以下のような条件を満たす辞書型の変数となる。np.nan
だったアイテムのインデックス
Iu_not
があるのでこれを用いるscores
をバリューによって並び替えればよい。引数reverse
をTrue
にすることで降順にする点もポイント。order
関数を作成し、実行結果を確認している。cos
関数をそのまま実行するとprint
文が実行されてしまうので、cos2
関数を用意してプリントされないようにしている。def cos2(pu, xi):
"""
コサイン類似度関数:ユーザプロファイルpuとアイテムiの特徴ベクトルxiのコサイン類似度を返す。
Parameters
----------
pu : ndarray
ユーザuのユーザプロファイル
xi : ndarray
アイテムiの特徴ベクトル
Returns
-------
float
コサイン類似度
"""
num = pu@xi
den_u = np.linalg.norm(pu, ord=2)
den_i = np.linalg.norm(xi, ord=2)
cosine = num / (den_u * den_i)
return cosine
def score(u, i):
"""
スコア関数:ユーザuのアイテムiに対するスコアを返す。
Parameters
----------
u : int
ユーザuのID(ダミー)
i : int
アイテムiのID
Returns
-------
float
スコア
"""
return cos2(pu, x[i])
def order(u, I):
"""
順序付け関数:アイテム集合Iにおいて、ユーザu向けの推薦リストを返す。
Parameters
----------
u : int
ユーザuのID
I : ndarray
アイテム集合
Returns
-------
list
タプル(アイテムID: スコア)を要素にした推薦リスト
"""
scores = {i: score(u, i) for i in I}
print('scores = ')
pprint.pprint(scores)
rec_list = sorted(scores.items(), key=lambda x:x[1], reverse=True)[:TOP_K]
return rec_list
u = 0
rec_list = order(u, Iu_not)
print('rec_list = ')
for i, scr in rec_list:
print('{}: {:.3f}'.format(i, scr))
import pprint
import numpy as np
np.set_printoptions(precision=3)
# 上位K件
TOP_K = 3
# 近傍アイテム数
K_ITEMS = 3
# しきい値
THETA = 0
Du = np.array([
[5, 3, +1],
[6, 2, +1],
[4, 1, +1],
[8, 5, -1],
[2, 4, -1],
[3, 6, -1],
[7, 6, -1],
[4, 2, np.nan],
[5, 1, np.nan],
[8, 6, np.nan],
[3, 4, np.nan],
[4, 7, np.nan],
[4, 4, np.nan],
])
I = np.arange(Du.shape[0])
x = Du[:,:-1]
ru = Du[:,-1]
Iu = I[~np.isnan(ru)]
Iup = I[ru==+1]
Iun = I[ru==-1]
Iu_not = np.setdiff1d(I, Iu)
dist
を完成させる。dist
では2つのベクトルを入力としてそのユークリッド距離を返すものなので、2つのベクトルの差に対してnp.linalg.norm
を使えばよいはずだ。def dist(xi, xj):
"""
距離関数:アイテムiの特徴ベクトルxiとアイテムjの特徴ベクトルxjのユークリッド距離を返す。
Parameters
----------
xi : ndarray
アイテムiの特徴ベクトル
xj : ndarray
アイテムjの特徴ベクトル
Returns
-------
float
ユークリッド距離
"""
distance = np.linalg.norm(xi-xj, ord = 2)
return distance
i = 7
j = 2
print(f'dist(x{i}, x{j}) = {dist(x[i], x[j]):.3f}')
i = 7
j = 3
print(f'dist(x{i}, x{j}) = {dist(x[i], x[j]):.3f}')
Iu
とIu_not
の両方からアイテムを取り出し、アイテムの持つベクトルの距離をdist
関数を使って求める問題である。とりあえずnp.array()
により2重内包表記の結果を包むことで答えと同じものが返ってくるようにはしたけど、題意的には正解と言いがたいかもしれない。あと、正直D[np.ix_(Iu_not,Iu)]
がよくわかっていない。D = np.array([[dist(x[i], x[j]) for i in Iu] for j in Iu_not])
print(f'D = \n{D}')
x
の各行xindex
とIu
に含まれる各アイテムiu
の持つベクトルx[iu]
の距離を求めて02と同じようにndarray
に変換し、argsort
によって昇順に番号をつけることで望みの結果を得た。Ii = np.array([[dist(xindex, x[iu]) for iu in Iu] for xindex in x]).argsort()
print(f'Ii = \n{Ii}')
TOP_K
列分だけを抽出する。これを使って評価値が+1
だったアイテムと近いアイテムがどれであったかを確認することができる。各行の持つ数値の中に3以上のものがあったのならば、「嫌い」と評価したアイテムが最近傍の3件に含まれていることを示している。Ii = Ii[:, :TOP_K]
print(f'Ii = \n{Ii}')
Iu_not
からアイテムをとってきて、保存したIi
からアイテムで行を指定して抽出することを考える。Ii = {i: Ii[i] for i in Iu_not}
print('Ii = ')
pprint.pprint(Ii)
predict1
を完成させる。Ii[i]
にnp.isin()
を使ったインデキシングをすることで答えを得る。Ii[i]
はアイテム集合のうち、距離が近かったアイテム3件の中に「好き」と評価したアイテムが入っているかどうかを判断するための数値が入っている。Iip
とIin
に対してnp.size()
を使うことでの近傍アイテム集合中に「好き」と評価したアイテムがいくつあるか、「嫌い」と評価したアイテムがいくつあるかがわかるので、その結果に対して素直にif文を使ってあげればよい。def predict1(u, i):
"""
予測関数(多数決方式):多数決方式によりユーザuのアイテムiに対する予測評価値を返す。
Parameters
----------
u : int
ユーザuのID(ダミー)
i : int
アイテムiのID
Returns
-------
float
予測評価値
"""
Iip = Ii[i][np.isin(Ii[i], Iup)]
print(f'I{i}+ = {Iip}')
Iin = Ii[i][np.isin(Ii[i], Iun)]
print(f'I{i}- = {Iin}')
if Iip.size > Iin.size:
rui = 1
elif Iip.size < Iin.size:
rui = -1
else:
rui = 0
return rui
u = 0
i = 7
print(f'predict1({u}, {i}) = {predict1(u, i):.3f}')
u = 0
i = 9
print(f'predict1({u}, {i}) = {predict1(u, i):.3f}')
predict1
を参考に、rui
の部分を平均方式に修正したものを関数predict2
として作成する。def predict2(u, i):
"""
予測関数(多数決方式):多数決方式によりユーザuのアイテムiに対する予測評価値を返す。
Parameters
----------
u : int
ユーザuのID(ダミー)
i : int
アイテムiのID
Returns
-------
float
予測評価値
"""
Iip = Ii[i][np.isin(Ii[i], Iup)]
Iin = Ii[i][np.isin(Ii[i], Iun)]
rui = (Iip.size - Iin.size)/K_ITEMS
return rui
u = 0
i = 7
print(f'predict2({u}, {i}) = {predict1(u, i):.3f}')
u = 0
i = 9
print(f'predict2({u}, {i}) = {predict1(u, i):.3f}')
order
を完成させる。そのために、scores
に格納された値score(u, i)
がTHETA
未満になるようなペアを除くための工夫を考える。score
はpredict2
関数で作るものとして、scores
の部分をいじる必要がある。ここでは辞書内包表記の中でif文を用いることによってTHETA
未満の値をすべて削除することを考える。def score(u, i):
"""
スコア関数:ユーザuのアイテムiに対するスコアを返す。
Parameters
----------
u : int
ユーザuのID
i : int
アイテムiのID
Returns
-------
float
スコア
"""
return predict2(u, i)
def order(u, I):
"""
順序付け関数:アイテム集合Iにおいて、ユーザu向けの推薦リストを返す。
Parameters
----------
u : int
ユーザuのID
I : ndarray
アイテム集合
Returns
-------
list
タプル(アイテムID: スコア)を要素にした推薦リスト
"""
scores = {i: score(u, i) for i in I if score(u, i) >= THETA}
rec_list = sorted(scores.items(), key=lambda x:x[1], reverse=True)[:TOP_K]
return rec_list
u = 0
rec_list = order(u, Iu_not)
print('rec_list = ')
for i, scr in rec_list:
print('{}: {:.3f}'.format(i, scr))
import pprint
import numpy as np
np.set_printoptions(precision=3)
# 近傍ユーザ数
K_USERS = 3
# 閾値
THETA = 0
R = np.array([
[np.nan, 4, 3, 1, 2, np.nan],
[5, 5, 4, np.nan, 3, 3 ],
[4, np.nan, 5, 3, 2, np.nan],
[np.nan, 3, np.nan, 2, 1, 1 ],
[2, 1, 2, 4, np.nan, 3 ],
])
U = np.arange(R.shape[0])
I = np.arange(R.shape[1])
Ui = [U[~np.isnan(R)[:,i]] for i in I]
Iu = [I[~np.isnan(R)[u,:]] for u in U]
ru_mean = np.nanmean(R, axis=1)
R2 = R - ru_mean.reshape((ru_mean.size, 1))
pearson1
を完成させる。Iuv
は、ユーザやユーザによる評価値が存在する列のみを集めたIu[u]
やIu[v]
の共通項をとることで比較可能なアイテムの評価を取得したものである。これに対し、各ユーザの評価を平均したものを作って差をとり、内積を計算すればよい。rui
として、それに対してnp.linalg.norm(uri, ord = 2)
を使ってあげればよい。rvi
として、それに対してnp.linalg.norm(uvi, ord = 2)
を使ってあげればよい。def pearson1(u, v):
"""
評価値行列Rにおけるユーザuとユーザvのピアソンの相関係数を返す。
Parameters
----------
u : int
ユーザuのID
v : int
ユーザvのID
Returns
-------
float
ピアソンの相関係数
"""
Iuv = np.intersect1d(Iu[u], Iu[v])
# R[u][Iu[u]]とR[v][Iu[v]]のIu[u]とIu[v]の部分をIuvにしてしまい時間を溶かした。
# ユーザの評価はすべて平均に反映する点に注意が必要かも。
rui = R[u][Iuv] - R[u][Iu[u]].mean()
rvi = R[v][Iuv] - R[v][Iu[v]].mean()
num = np.dot(rui, rvi)
print(f'num = {num}')
den_u = np.linalg.norm(rui, ord = 2)
print(f'den_u = {den_u:.3f}')
den_v = np.linalg.norm(rvi, ord = 2)
print(f'den_v = {den_v:.3f}')
prsn = num / (den_u * den_v)
return prsn
u = 0
v = 1
prsn = pearson1(u, v)
print(f'pearson1({u}, {v}) = {prsn:.3f}')
pearson2
を完成させる。R
でなくR2
を利用するところが大きな変更点となる。rui
として、それに対してnp.linalg.norm(uri, ord = 2)
を使ってあげればよい。02と違い、R2
を使う点がポイントとなっている。rvi
として、それに対してnp.linalg.norm(uvi, ord = 2)
を使ってあげればよい。R2
を使う点がポイント。def pearson2(u, v):
"""
平均中心化評価値行列R2におけるユーザuとユーザvのピアソンの相関係数を返す。
Parameters
----------
u : int
ユーザuのID
v : int
ユーザvのID
Returns
-------
float
ピアソンの相関係数
"""
Iuv = np.intersect1d(Iu[u], Iu[v])
rui = R2[u][Iuv]
rvi = R2[v][Iuv]
num = np.dot(rui, rvi)
print('num = {}'.format(num))
den_u = np.linalg.norm(rui, ord = 2)
print('den_u = {:.3f}'.format(den_u))
den_v = np.linalg.norm(rvi, ord = 2)
print('den_v = {:.3f}'.format(den_v))
prsn = num / (den_u * den_v)
return prsn
u = 0
v = 1
prsn = pearson2(u, v)
print(f'pearson2({u}, {v}) = {prsn:.3f}')
R2
によって計算するという問題。内包表記で2重リストを作って、np.array()
すればよさそう。以下では、それらに加えて関数sim
で利用するのが関数pearson2
だと各種print
が発生する問題があったのでそれを取り除いた関数pearson3
を別途作って対応した。def pearson3(u, v):
"""
平均中心化評価値行列R2におけるユーザuとユーザvのピアソンの相関係数を返す。
Parameters
----------
u : int
ユーザuのID
v : int
ユーザvのID
Returns
-------
float
ピアソンの相関係数
"""
Iuv = np.intersect1d(Iu[u], Iu[v])
rui = R2[u][Iuv]
rvi = R2[v][Iuv]
num = np.dot(rui, rvi)
den_u = np.linalg.norm(rui, ord = 2)
den_v = np.linalg.norm(rvi, ord = 2)
prsn = num / (den_u * den_v)
return prsn
def sim(u, v):
"""
ユーザ類似度関数:ユーザuとユーザvのユーザ類似度を返す。
Parameters
----------
u : int
ユーザuのID
v : int
ユーザvのID
Returns
-------
float
ユーザ類似度
"""
return pearson3(u, v)
S = np.array([[sim (u, v) for u in U] for v in U])
print(f'S = \n{S}')
S
から作った辞書が与えられていて、キーはユーザ、バリューはまた別の辞書となっていることがUu
からわかる。Uu
のバリューの中で各キーに対して格納されている辞書を類似度の順番に並べて上位K_USERS
件のみを残すというものであるため、その辞書のソートを行う。Uu
のバリュー側に格納された辞書について、条件式を使ってそのバリューの値がTHETA
未満のものを除外する。Uu = {u: {v: S[u,v] for v in U if u!=v} for u in U}
print('Uu = ')
pprint.pprint(Uu)
Uu = {u: dict(sorted(Uu[u].items(), key=lambda x:x[1], reverse=True)[:K_USERS]) for u in Uu.keys()}
print('Uu = ')
pprint.pprint(Uu)
Uu = {u: {v[0]: v[1] for v in Uu[u].items() if v[1] >= THETA} for u in Uu.keys()}
print('Uu = ')
pprint.pprint(Uu)
Uu = {u: np.array(list(Uu[u].keys())) for u in U}
print('Uu = ')
pprint.pprint(Uu)
predict
について完成させる。Uui
は、アイテムi
を評価したユーザUi[i]
とユーザu
の近傍にいるユーザUu[u]
の共通点を、np.intersect1d()
を使って求めることで作ることができる。Uui
の大きさが0
でなかった場合に発生する複雑そうな項を作っていく。sim(u, v)
はS[u, v]
であったので、u
に対するUui
をインデックスに指定してあげてndarray
状のベクトルを取得して計算に用いる。についてはR2[v, i]
が相当する部分だったはずなのでv
部分をUui
で指定してあげて計算用のndarray
を取得する。こうして取得したベクトルを、前者のndarray
は分子の内積計算と分母の絶対値をとった和の計算に、後者のndarray
は分子の内積計算に用いればよい。def predict(u, i):
"""
予測関数:ユーザuのアイテムiに対する予測評価値を返す。
Parameters
----------
u : int
ユーザuのID
i : int
アイテムiのID
Returns
-------
float
ユーザuのアイテムiに対する予測評価値
"""
Uui = np.intersect1d(Uu[u], Ui[i])
print(f'U{u}{i} = {Uui}')
if Uui.size <= 0:
return ru_mean[u]
else:
rui_pred = ru_mean[u] + np.dot(S[u][Uui], R2[:, i][Uui])/np.abs(S[u][Uui]).sum()
return rui_pred
u = 0
i = 0
print('r{}{} = {:.3f}'.format(u, i, predict(u, i)))
u = 0
i = 5
print('r{}{} = {:.3f}'.format(u, i, predict(u, i)))
R
に対して、np.nan
の部分のみを予測値predict(u, i)
で埋めてあげればよい。余計なprintが発生する関係で、ここではpredict12
関数を別途用意した上で、2重内包表記の各成分を求めつつ最後にnp.array()
でndarray
としたR3
を作った。def predict12(u, i):
"""
予測関数:ユーザuのアイテムiに対する予測評価値を返す。
Parameters
----------
u : int
ユーザuのID
i : int
アイテムiのID
Returns
-------
float
ユーザuのアイテムiに対する予測評価値
"""
Uui = np.intersect1d(Uu[u], Ui[i])
if Uui.size <= 0:
return ru_mean[u]
else:
rui_pred = ru_mean[u] + np.dot(S[u][Uui], R2[:, i][Uui])/np.abs(S[u][Uui]).sum()
return rui_pred
R3 = np.array([[predict12(r0, r1) if np.isnan(R[r0, r1]) else R[r0, r1] for r1 in range(R.shape[1])] for r0 in range(R.shape[0])])
print(f'R\'\' = \n{R3}')
import pprint
import numpy as np
np.set_printoptions(precision=3)
# 近傍アイテム数
K_ITEMS = 3
# 閾値
THETA = 0
R = np.array([
[np.nan, 4, 3, 1, 2, np.nan],
[5, 5, 4, np.nan, 3, 3 ],
[4, np.nan, 5, 3, 2, np.nan],
[np.nan, 3, np.nan, 2, 1, 1 ],
[2, 1, 2, 4, np.nan, 3 ],
])
U = np.arange(R.shape[0])
I = np.arange(R.shape[1])
Ui = [U[~np.isnan(R)[:,i]] for i in I]
Iu = [I[~np.isnan(R)[u,:]] for u in U]
ru_mean = np.nanmean(R, axis=1)
R2 = R - ru_mean.reshape((ru_mean.size, 1))
cos
を完成させていく。することとしては、内積をとるためのベクトルをとるためのインデックスはUij
に用意されているのでそれを用いてR
からi
とj
のベクトルを取得することである。def cos(i, j):
"""
評価値行列Rにおけるアイテムiとアイテムjのコサイン類似度を返す。
Parameters
----------
i : int
アイテムiのID
j : int
アイテムjのID
Returns
-------
float
コサイン類似度
"""
Uij = np.intersect1d(Ui[i], Ui[j])
cosine = np.dot(R[Uij, i], R[Uij, j]) / (np.linalg.norm(R[Uij, i]) * np.linalg.norm(R[Uij, j]))
return cosine
i = 0
j = 4
cosine = cos(i, j)
print(f'cos({i}, {j}) = {cosine:.3f}')
adjusted_cos
を完成させていく。することとしては、問題01で作った部分をR
ではなくR2
を利用する形に書き直せばよい。def adjusted_cos(i, j):
"""
評価値行列R2におけるアイテムiとアイテムjの調整コサイン類似度を返す。
Parameters
----------
i : int
アイテムiのID
j : int
アイテムjのID
Returns
-------
cosine : float
調整コサイン類似度
"""
Uij = np.intersect1d(Ui[i], Ui[j])
Uij = np.intersect1d(Ui[i], Ui[j])
cosine = np.dot(R2[Uij, i], R2[Uij, j]) / (np.linalg.norm(R2[Uij, i]) * np.linalg.norm(R2[Uij, j]))
return cosine
i = 0
j = 4
cosine = adjusted_cos(i, j)
print(f'adjusted_cos({i}, {j}) = {cosine:.3f}')
sim
による計算によって与えればよい。def sim(i, j):
"""
アイテム類似度関数:アイテムiとアイテムjのアイテム類似度を返す。
Parameters
----------
i : int
アイテムiのID
j : int
アイテムjのID
Returns
-------
float
アイテム類似度
"""
return adjusted_cos(i, j)
S = np.array([[sim(i, j) for i in I] for j in I])
print('S = \n{}'.format(S))
S
から作った辞書が与えられていて、キーはユーザ、バリューはまた別の辞書となっていることがIi
からわかる。これに対して、少し複雑だが入れ子になっているほうの辞書のバリューによって並べ替えを行う。Ii
のバリュー側に格納された辞書について、条件式を使ってそのバリューの値がTHETA
未満のものを除外する。# アイテム-アイテム類似度行列から対象アイテムを除外した辞書
Ii = {i: {j: S[i,j] for j in I if i != j} for i in I}
print('Ii = ')
pprint.pprint(Ii)
Ii = {i[0]: dict(sorted(i[1].items(), key=lambda x:x[1], reverse=True)[:K_USERS]) for i in Ii.items()}
print('Ii = ')
pprint.pprint(Ii)
Ii = {i: {j[0]: j[1] for j in Ii[i].items() if j[1] >= THETA} for i in Ii.keys()}
print('Ii = ')
pprint.pprint(Ii)
# 各アイテムの類似アイテム集合をまとめた辞書
Ii = {i: np.array(list(Ii[i].keys())) for i in I}
print('Ii = ')
pprint.pprint(Ii)
Iiu
は、ユーザu
が評価したアイテムIu[u]
とアイテムの近傍にあるアイテムIi[i]
の共通点を、np.intersect1d()
を使って求めることで作ることができる。Iiu
の大きさが0
でなかった場合に発生する複雑そうな項を作っていく。やっていることは基本的に第5章の11に書いた操作で、対象の変数を変えた以外は同じと考えてもさしつかえないように思う。def predict(u, i):
"""
予測関数:ユーザuのアイテムiに対する予測評価値を返す。
Parameters
----------
u : int
ユーザuのID
i : int
アイテムiのID
Returns
-------
float
ユーザuのアイテムiに対する予測評価値
"""
Iiu = np.intersect1d(Ii[i], Iu[u])
print(f'I{i}{u} = {Iiu}')
if Iiu.size <= 0:
return ru_mean[u]
else:
rui_pred = ru_mean[u] + np.dot(S[i][Iiu], R2[u, :][Iiu])/np.abs(S[i][Iiu]).sum()
return rui_pred
u = 0
i = 0
print(f'r{u}{i} = {predict(u, i):.3f}')
u = 0
i = 5
print(f'r{u}{i} = {predict(u, i):.3f}')
import numpy as np
import numpy.linalg as LA
np.set_printoptions(precision=3)
# 縮約後の次元数
DIM = 2
Du = np.array([
[5, 3, 3, +1],
[6, 2, 5, +1],
[4, 1, 5, +1],
[8, 5, 9, -1],
[2, 4, 2, -1],
[3, 6, 5, -1],
[7, 6, 8, -1],
[4, 2, 3, np.nan],
[5, 1, 8, np.nan],
[8, 6, 6, np.nan],
[3, 4, 2, np.nan],
[4, 7, 5, np.nan],
[4, 4, 4, np.nan],
])
I = np.arange(Du.shape[0])
x = Du[:,:-1]
ru = Du[:,-1]
x
の行方向つまりaxis=0
について平均をとればよい。xk_mean = x.mean(axis=0)
print('xk_mean = {}'.format(xk_mean))
s2 = x.var(axis=0)
print(f's^2 = {s2}')
numpy
のブロードキャストによって単純に差や商をとる計算が使えるので普通に計算すればよい。一応、np.sqrt()
を使う必要があることに注意。x2 = (x - xk_mean) / np.sqrt(s2)
print(f'x\' = \n{x2}')
x2
を用いて共分散を求めるとよい。np.cov
の引数でbias=True
を用いることで不偏分散ではない分散を求めることができる。k = 0
l = 1
skl = np.cov(x2[:, k], x2[:, l], bias = True)[0, 1]
print(f's{k}{l} = {skl:.3f}')
np.cov
を使って分散共分散行列を求めればよい。ただ、np.cov
にそのままx2
を入れると行対行の分散共分散行列が作られるのでx2.T
という転置したものを引数に指定する。S = np.cov(x2.T, bias = True)
print(f'S = \n{S}')
np.linalg.eig()
を利用して固有値と固有ベクトルを求める。lmd, v = LA.eig(S)
print(f'λ = {lmd}')
print(f'v = \n{v}')
np.argsort()
を用いればよいのだが、今回は降順なのでlmd
を引数に指定する際に符号を逆にしてやる必要がある。indices = np.argsort(-lmd)
print(f'indices = {indices}')
lmd
のインデキシングにindices
を使ってあげればよい。lmd = lmd[indices]
print(f'λ = {lmd}')
v
の列方向について、indices
でその順番を指定してあげればよい。v = v[:, indices]
print(f'v = \n{v}')
v
のDIM
列めまでをスライシングすることによってV
を作ればよい。V = v[:, :DIM]
print(f'V = \n{V}')
x2
のi
行めとv
のk
列目の内積で得られるのでその通りに計算する。i = 0
k = 0
xik3 = np.dot(x2[i, :] ,v[:, k])
print(f'x{i}{k}\'\' = {xik3:.3f}')
@
演算子を使って、x2
とV
の行列計算を行えばよい。x3 = x2 @ V
print(f'x\'\' = \n{x3}')
0
になることもなく安心して割合の計算を行うことができたはず。# k=0とすると第1主成分になる。おそらくインデックスの都合。
k = 0
pk = lmd[k]/lmd.sum()
print(f'第{k+1}主成分の寄与率 = {pk:.3f}')
pk
の分子の部分をスライスした部分の和としてあげればよい。k = 2
ck = lmd[:k].sum()/lmd.sum()
print(f'第{k}主成分までの累積寄与率 = {ck:.3f}')
np.hstack()
を使ってx3
とru
を横に結合する。np.hstack()
の引数に()
を入れないといけないことや、ru
にreshape
を使う必要があることに注意。Du2 = np.hstack((x3, ru.reshape(-1, 1)))
print(f'Du\' = \n{Du2}')
import numpy as np
import numpy.linalg as LA
np.set_printoptions(precision=3)
# 縮約後の次元数
DIM = 2
R = np.array([
[np.nan, 4, 3, 1, 2, np.nan],
[5, 5, 4, np.nan, 3, 3 ],
[4, np.nan, 5, 3, 2, np.nan],
[np.nan, 3, np.nan, 2, 1, 1 ],
[2, 1, 2, 4, np.nan, 3 ],
])
U = np.arange(R.shape[0])
I = np.arange(R.shape[1])
Ui = [U[~np.isnan(R)[:,i]] for i in I]
Iu = [I[~np.isnan(R)[u,:]] for u in U]
ru_mean = np.nanmean(R, axis=1)
R2 = R - ru_mean.reshape((ru_mean.size, 1))
R2
で行方向に平均をとればよい。np.nanmean()
によってnp.nan
の処理を行う必要があることも忘れずに。ri2_mean = np.nanmean(R2,axis = 0)
print('ri2_mean = {}'.format(ri2_mean))
s2 = np.nanvar(R2,axis = 0)
print(f's^2 = {s2}')
np.nan
が絡むので今回はnp.cov
ではなくnp.dot
で内積をとる形で値を求める。R2[Uij, i]
でi
列目かつi
列目とj
列目でnp.nan
でない行のみを取得することができるのだが、数式の定義にもあるように平均との差分をとる部分とUij
の要素数で割る部分を忘れずに実装する。i = 0
j = 1
Uij = np.intersect1d(Ui[i], Ui[j])
sij = np.dot(R2[Uij, i] - np.nanmean(R2[:, i]), R2[Uij, j] - np.nanmean(R2[:, j]))/Uij.size
print(f's{i}{j} = {sij:.3f}')
my_sij
は途中のUij
のサイズが0
の場合S
の値を0
に変更する必要があるためUij.size
も返り値にふくめているが結局関数の中でゼロ割が起こるのでそこで発生したnp.nan
を0
にしてしまうやり方のほうがよさそうだなと思った。def my_sij(i, j):
Uij = np.intersect1d(Ui[i], Ui[j])
sij = np.dot(R2[Uij, i] - np.nanmean(R2[:, i]), R2[Uij, j] - np.nanmean(R2[:, j]))/Uij.size
return Uij.size, sij
S = np.array([[my_sij(i, j)[1] if my_sij(i, j)[0] else 0 for i in I] for j in I])
print(f'S = \n{S}')
LA.eig()
で計算すればよい。v
の値の符号が逆になってしまっているのだが、これは問題ないはず。lmd, v = LA.eig(S)
print(f'λ = {lmd}')
print(f'v = \n{v}')
v
について固有値の大きさ順に列を並び替えた上で、DIM
列までをスライシングすればよい。indices = np.argsort(-lmd)
V = v[:, indices][:, :DIM]
print(f'V = \n{V}')
R2[u, :]
やV[:, k]
を計算するとnp.nan
のせいでよくないことに気をつけなければならない。u = 0
k = 0
puk = np.dot(R2[u, Iu[u]] ,V[Iu[u], k])/Iu[u].size
print(f'p{u}{k} = {puk:.3f}')
R2
やV
を計算するとnp.nan
のせいでよくないのでnp.nan
を排除して2重リスト内包表記で求めることになる。今回は関数my_puk
を用意してnp.nan
を排除した上で計算を行う。def my_puk(u, k):
puk = np.dot(R2[u, Iu[u]], V[Iu[u], k]) / Iu[u].size
return puk
P = np.array([[my_puk(u, k) for k in range(DIM)] for u in U])
print(f'P = \n{P}')
import pprint
import numpy as np
from fractions import Fraction
# 上位K件
TOP_K = 3
# スムージングパラメタ
ALPHA = 1
# クラス数
N = 2
# 各特徴量がとりうる値のユニーク数
M = [2, 2, 2, 2, 2, 2]
# しきい値
THETA = 0.5
Du = np.array([
[1, 0, 0, 0, 1, 0, +1],
[0, 1, 0, 0, 1, 0, +1],
[1, 1, 0, 0, 1, 0, +1],
[1, 0, 0, 1, 1, 0, +1],
[1, 0, 0, 0, 0, 1, +1],
[0, 1, 0, 1, 0, 1, +1],
[0, 0, 1, 0, 1, 0, -1],
[0, 0, 1, 1, 1, 0, -1],
[0, 1, 0, 0, 1, 1, -1],
[0, 0, 1, 0, 0, 1, -1],
[1, 1, 0, 1, 1, 0, np.nan],
[0, 0, 1, 0, 1, 1, np.nan],
[0, 1, 1, 1, 1, 0, np.nan],
])
I = np.arange(Du.shape[0])
x = Du[:,:-1]
ru = Du[:,-1]
Iu = I[~np.isnan(ru)]
Iu_not = np.setdiff1d(I, Iu)
DuL = Du[Iu]
xL = x[Iu]
ruL = ru[Iu]
DuU = Du[Iu_not]
xU = x[Iu_not]
P_prior
、P_cond
、P
を完成させていく。# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
np.where(ru == r, 1, 0).sum()
# 以下、試しに計算するのに用いたコードとその計算結果
ruL.size
# 問題01と02の結果を利用したもの。スムージングない版。
def P_prior(r):
"""
評価値がrとなる事前確率を返す。
Parameters
----------
r : int
評価値
Returns
-------
Fraction
事前確率
"""
num = np.where(ru == r, 1, 0).sum()
den = ruL.size
prob = Fraction(num, den, _normalize=False)
return prob
r = +1
print(f'P(R={r:+}) = {P_prior(r)}')
r = -1
print(f'P(R={r:+}) = {P_prior(r)}')
# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
k = 1
i = 2
Du[:, k][(Du[i, k] == Du[:, k])&(ru == r)].size
# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
ru[ru == r].size
# 問題03と04の結果を利用したもの。スムージングない版。
def P_cond(i, k, r):
"""
評価値がrとなる条件下でアイテムiの特徴量kに関する条件付き確率を返す。
Parameters
----------
i : int
アイテムiのID
k : int
特徴量kのインデックス
r : int
評価値
Returns
-------
Fraction
条件付き確率
"""
num = Du[:, k][(Du[i, k] == Du[:, k])&(ru == r)].size
den = ru[ru == r].size
prob = Fraction(num, den, _normalize=False)
return prob
i = 10
k = 0
r = +1
print('P(X{}=x{},{}|R={:+}) = {}'.format(k, i, k, r, P_cond(i, k, r)))
r = -1
print('P(X{}=x{},{}|R={:+}) = {}'.format(k, i, k, r, P_cond(i, k, r)))
pp
と、各特徴量に関する条件付き確率の積を書けることで計算すればよい。warningが発生しているが、解決は時間があればまた後でおこなうつもり。def P(i, r):
"""
アイテムiの評価値がrとなる確率を返す。
Parameters
----------
i : int
アイテムiのID
r : int
評価値
Returns
-------
Fraction
事前確率
list of Fraction
各特徴量に関する条件付き確率
float
好き嫌いの確率
"""
pp = P_prior(r)
pk = [P_cond(i, k, r) for k in range(0, x.shape[1])]
prob = float(pp) * np.array([float(p) for p in pk]).prod()
return pp, pk, prob
i = 10
r = +1
pp, pk, prob = P(i, r)
left = 'P(R={:+}|'.format(r) + ','.join(map(str, map(int, x[i]))) + ')'
right = str(pp) + '×' + '×'.join(map(str, pk))
print(f'{left} = {right} = {prob:.3f}')
r = -1
pp, pk, prob = P(i, r)
left = 'P(R={:+}|'.format(r) + ','.join(map(str, map(int, x[i]))) + ')'
right = str(pp) + '×' + '×'.join(map(str, pk))
print(f'{left} = {right} = {prob:.3f}')
ALPHA
だけは足すようにする。# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
np.where(ru == r, 1, 0).sum() + ALPHA
N*ALPHA
だけは足すようにする。# 以下、試しに計算するのに用いたコードとその計算結果
ruL.size + N * ALPHA
# 問題06と07の結果を利用したもの。スムージングあり版。
def P_prior(r):
"""
評価値がrとなる事前確率を返す。
Parameters
----------
r : int
評価値
Returns
-------
Fraction
事前確率
"""
num = np.where(ru == r, 1, 0).sum() + ALPHA
den = ruL.size + N * ALPHA
prob = Fraction(num, den, _normalize=False)
return prob
ALPHA
だけは足すようにする。# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
k = 1
i = 2
Du[:, k][(Du[i, k] == Du[:, k])&(ru == r)].size + ALPHA
ALPHA
とM
の第成分の積だけは忘れず足すようにする。# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
k = 1
ru[ru == r].size + ALPHA * M[k]
# 問題08と09の結果を利用したもの。スムージングあり版。
def P_cond(i, k, r):
"""
評価値がrとなる条件下でアイテムiの特徴量kに関する条件付き確率を返す。
Parameters
----------
i : int
アイテムiのID
k : int
特徴量kのインデックス
r : int
評価値
Returns
-------
Fraction
条件付き確率
"""
num = Du[:, k][(Du[i, k] == Du[:, k])&(ru == r)].size + ALPHA
den = ru[ru == r].size + ALPHA * M[k]
prob = Fraction(num, den, _normalize=False)
return prob
score
を完成させる。やることとしては問題05でみたP(i, r)
を使って、P(i, +1)[2] / (P(i, +1)[2] + P(i, -1)[2])
を計算してあげればよい。def score(u, i):
"""
スコア関数:ユーザuのアイテムiに対するスコアを返す。
Parameters
----------
u : int
ユーザuのID(ダミー)
i : int
アイテムiのID
Returns
-------
float
スコア
"""
scr = P(i, +1)[2] / (P(i, +1)[2] + P(i, -1)[2])
return scr
u = 0
scores = {i: score(u, i) for i in Iu_not}
print('scores = ')
pprint.pprint(scores)
scores
の作り方をいじって、if文によりTHETA
よりもscore(u, i)
が小さいものを除去してやる必要がある。def order(u, I):
"""
順序付け関数:アイテム集合Iにおいて、ユーザu向けの推薦リストを返す。
Parameters
----------
u : int
ユーザuのID
I : ndarray
アイテム集合
Returns
-------
list
タプル(アイテムID: スコア)を要素にした推薦リスト
"""
scores = {i: score(u, i) for i in I if score(u, i) >= THETA}
rec_list = sorted(scores.items(), key=lambda x:x[1], reverse=True)[:TOP_K]
return rec_list
u = 0
rec_list = order(u, Iu_not)
print('rec_list = ')
for i, scr in rec_list:
print('{}: {:.3f}'.format(i, scr))
import pprint
import numpy as np
Du = np.array([
[1, 0, 0, 0, 1, 0, +1],
[0, 1, 0, 0, 1, 0, +1],
[1, 1, 0, 0, 1, 0, +1],
[1, 0, 0, 1, 1, 0, +1],
[1, 0, 0, 0, 0, 1, +1],
[0, 1, 0, 1, 0, 1, +1],
[0, 0, 1, 0, 1, 0, -1],
[0, 0, 1, 1, 1, 0, -1],
[0, 1, 0, 0, 1, 1, -1],
[0, 0, 1, 0, 0, 1, -1],
[1, 1, 0, 1, 1, 0, np.nan],
[0, 0, 1, 0, 1, 1, np.nan],
[0, 1, 1, 1, 1, 0, np.nan],
])
I = np.arange(Du.shape[0])
x = Du[:,:-1]
ru = Du[:,-1]
Iu = I[~np.isnan(ru)]
Iu_not = np.setdiff1d(I, Iu)
DuL = Du[Iu]
xL = x[Iu]
ruL = ru[Iu]
DuU = Du[Iu_not]
xU = x[Iu_not]
G
を完成させる。問題01では、評価値を与えられたアイテムの数で「好き」と評価されたアイテムの数を割ってあげてpp
を計算する。def G(DL):
"""
訓練データDLのジニ係数を返す。
Parameters
----------
DL : ndarray
訓練データDL
Returns
-------
float
ジニ係数
ただし、DLに事例が含まれていないときは0
"""
if DL.shape[0] == 0:
return 0
r = DL[:,-1]
pp = DL[r == +1].shape[0] / DL.shape[0]
pn = DL[r == -1].shape[0] / DL.shape[0]
gini = 1 - (pp**2 + pn**2)
return gini
print(f'G(DuL) = {G(DuL):.3f}')
G_partitioned
とその結果を表示するためのコードを完成させる。問題04では、DuL
から訓練データから特徴量が0
となる行のみを抜き出すような処理を書く。特徴量の列に注目して、インデクシングを行えばよい。DuL
から訓練データから特徴量が1
となる行のみを抜き出すような処理を書く。特徴量の列に注目して、インデクシングを行えばよい。def G_partitioned(DL0, DL1):
"""
訓練データをDL0とDL1に分割したときのジニ係数を返す。
Parameters
----------
DL0 : ndarray
訓練データDL0
DL1 : ndarray
訓練データDL1
Returns
-------
float
ジニ係数
"""
gini = (DL0.shape[0] * G(DL0) + DL1.shape[0] * G(DL1)) / (DL0.shape[0] + DL1.shape[0])
return gini
# 特徴量kを含まない訓練事例集合
k = 0
DuL0 = DuL[DuL[:, k] == 0]
# 特徴量kを含む訓練事例集合
print('DuL0 = \n{}'.format(DuL0))
DuL1 = DuL[DuL[:, k] == 1]
# 特徴量kを基準に分割したときのジニ係数
print(f'DuL1 = \n{DuL1}')
print(f'G(DuL → [DuL0, DuL1]) = {G_partitioned(DuL0, DuL1):.3f}')
get_ginis
を使ってジニ係数を最小とするような特徴量を考える。辞書であるginis
から、バリューが最小値になるようなキーを求める。最小値をとるバリューに対応するキーを求める方法としてmin()
の引数key
にdictname.get
を用いる方法があることを以下のサイトで知ったのでそれを使う。def get_ginis(DL):
"""
訓練データDLを各特徴量で分割したときの(特徴量のインデックス: ジニ係数)をペアにした辞書を返す。
Parameters
----------
DL : ndarray
訓練データDL
Returns
-------
dict
(特徴量のインデックス: ジニ係数)をペアにした辞書
"""
ginis = {}
for k in range(0, x.shape[1]):
DL0 = DL[DL[:,k]==0]
DL1 = DL[DL[:,k]==1]
ginis[k] = G_partitioned(DL0, DL1)
return ginis
# レベル0(根ノード)の選択基準
ginis = get_ginis(DuL)
print('ginis = ')
pprint.pprint(ginis)
k0 = min(ginis, key=ginis.get)
print(f'k0 = {k0}')
DuL0 = DuL[DuL[:,k0]==0]
DuL1 = DuL[DuL[:,k0]==1]
print(f'DuL0 = \n{DuL0}')
print(f'DuL1 = \n{DuL1}')
DuL
ではなくDuL0
に対して行う。# レベル1a(レベル1の左端ノード)の選択基準
ginis1a = get_ginis(DuL0)
k1a = min(ginis1a, key=ginis1a.get)
print(f'k1a = {k1a}')
DuL00 = DuL0[DuL0[:,k1a] == 0]
DuL01 = DuL0[DuL0[:,k1a] == 1]
print(f'DuL00 = \n{DuL00}')
print(f'DuL01 = \n{DuL01}')
# レベル2a(レベル2の左端ノード)の選択基準
ginis2a = get_ginis(DuL00)
k2a = min(ginis2a, key=ginis2a.get)
print('k2a = {}'.format(k2a))
DuL000 = DuL00[DuL00[:,k2a] == 0]
DuL001 = DuL00[DuL00[:,k2a] == 1]
print(f'DuL000 = \n{DuL000}')
print(f'DuL001 = \n{DuL001}')
train
とpredict
を使って予測結果を出力する処理を実行する。def train(DL, key=0):
"""
学習関数:訓練データDLから決定木を学習する。
Parameters
----------
DL : ndarray
訓練データDL
key : int
キー値
"""
if len(DL) <= 0:
return
elif np.count_nonzero(DL[:,-1]==-1) <= 0:
dtree[key] = '+1'
return
elif np.count_nonzero(DL[:,-1]==+1) <= 0:
dtree[key] = '-1'
return
ginis = get_ginis(DL)
k = min(ginis, key=ginis.get)
dtree[key] = k
DL0 = DL[DL[:,k] == 0]
DL1 = DL[DL[:,k] == 1]
train(DL0, key * 2 + 1)
train(DL1, key * 2 + 2)
def predict(u, i, key=0):
"""
予測関数:ユーザuのアイテムiに対する予測評価値を返す。
Parameters
----------
u : int
ユーザuのID(ダミー)
i : int
アイテムiのID
key : int
キー値
Returns
-------
int
ユーザuのアイテムiに対する予測評価値
"""
if type(dtree[key]) == str: return int(dtree[key])
k = dtree[key]
if x[i,k] == 0:
return predict(u, i, key * 2 + 1)
elif x[i,k] == 1:
return predict(u, i, key * 2 + 2)
dtree = {}
train(DuL)
print(f'dtree = {dtree}')
u = 0
ruU_pred = {i: predict(u, i) for i in Iu_not}
print('ruU_pred = {}'.format(ruU_pred))
import numpy as np
# テストデータ
R = np.array([
[np.nan, 4, np.nan, np.nan, np.nan, np.nan, 2, np.nan, np.nan, np.nan],
[np.nan, np.nan, np.nan, np.nan, 2, np.nan, np.nan, np.nan, 5, np.nan],
[np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 3, np.nan, np.nan],
])
U = np.arange(R.shape[0])
I = np.arange(R.shape[1])
# 推薦システムAによる予測評価値
RA = np.array([
[np.nan, 2, np.nan, np.nan, np.nan, np.nan, 2, np.nan, np.nan, np.nan],
[np.nan, np.nan, np.nan, np.nan, 2, np.nan, np.nan, np.nan, 3, np.nan],
[np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 3, np.nan, np.nan],
])
# 推薦システムBによる予測評価値
RB = np.array([
[np.nan, 3, np.nan, np.nan, np.nan, np.nan, 1, np.nan, np.nan, np.nan],
[np.nan, np.nan, np.nan, np.nan, 3, np.nan, np.nan, np.nan, 4, np.nan],
[np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, 4, np.nan, np.nan],
])
RA
とR
を比較したMAEのMAE_A
を計算する。分母は非np.nan
であるデータの個数として、分子はR - RA
の絶対値をnp.abs(R - RA)
でとってあげて親の顔ほどはみたnp.nan
を無視した和をとることで計算できる。MAE_A = np.nansum(np.abs(R - RA)) / np.count_nonzero(~np.isnan(R))
print(f'MAE_{"A"} = {MAE_A:.3f}')
MAE_B = np.nansum(np.abs(R - RB)) / np.count_nonzero(~np.isnan(R))
print(f'MAE_{"B"} = {MAE_B:.3f}')
RA
とR
を比較したMSEのMSE_A
を計算する。分母は非np.nan
であるデータの個数として、分子はR - RA
の2乗誤差を(R - RA)**2
でとってあげて親の顔ほどはみたnp.nan
を無視した和をとることで計算できる。MSE_A = np.nansum((R - RA)**2) / np.count_nonzero(~np.isnan(R))
print(f'MSE_{"A"} = {MSE_A:.3f}')
MSE_B = np.nansum((R - RB)**2) / np.count_nonzero(~np.isnan(R))
print(f'MSE_{"B"} = {MSE_B:.3f}')
RMSE_A = np.sqrt(MSE_A)
print(f'RMSE_{"A"} = {RMSE_A:.3f}')
RMSE_B = np.sqrt(MSE_B)
print(f'RMSE_{"B"} = {RMSE_B:.3f}')
R_MAX
とR_MIN
をnp.nanmin()
やnp.nanmax()
で求めて、その差で割ればよい。# NMAE
N_MAX = np.nanmax(R)
N_MIN = np.nanmin(R)
NMAE_A = MAE_A / (N_MAX - N_MIN)
print(f'NMAE_{"A"} = {NMAE_A:.3f}')
NMAE_B = MAE_B / (N_MAX - N_MIN)
print(f'NMAE_{"B"} = {NMAE_B:.3f}')
# NRMSE
NRMSE_A = RMSE_A / (N_MAX - N_MIN)
print(f'NRMSE_{"A"} = {NRMSE_A:.3f}')
NRMSE_B = RMSE_B / (N_MAX - N_MIN)
print(f'NRMSE_{"B"} = {NRMSE_B:.3f}')
import numpy as np
# テストデータ
R = np.array([
[5, 4, 3, np.nan, 5, 4, 2, 2, np.nan, np.nan],
])
U = np.arange(R.shape[0])
I = np.arange(R.shape[1])
Iu = [I[~np.isnan(R)[u,:]] for u in U]
# 推薦システムAによる推薦リスト
RA = np.array([
[1, 6, 3, np.nan, 4, 2, 5, 7, np.nan, np.nan],
])
# 推薦システムBによる推薦リスト
RB = np.array([
[4, 3, 1, np.nan, 6, 7, 2, 5, np.nan, np.nan],
])
confusion_matrix
を完成させる。RS
に対して4
以上の値をとっていればTrue
、それ以外の場合はFalse
となるような変数like
を作る。今回はRS
のnp.nan
でないところのみを抽出したあとでインデキシングを使うことで作成した。np.argsort(-ndarray)
を使って、その結果がK
未満の成分のインデックスを取り出せばよい。np.logical_and(like, recommended)
を利用してlike
もrecommended
もTrue
であればTrue
を返すようなndarray
を作ればよい。これに対してTrue
の数を数えるには、np.count_nonzero()
を利用する。def confusion_matrix(u, RS, K):
"""
ユーザu向け推薦リストRSの上位K件における混同行列の各値を返す。
Parameters
----------
u : int
ユーザuのID
RS : ndarray
推薦リストRS
K : int
上位K件
Returns
-------
int
TP
int
FN
int
FP
int
TN
"""
Ru = R[~np.isnan(R)]
like = Ru >= 4
print(f'like = {like}')
RSu = RS[~np.isnan(RS)]
recommended = RSu <= K
print(f'recommended@{K} = {recommended}')
TP = np.count_nonzero(np.logical_and(like, recommended))
print(f'TP@{K} = {TP}')
FN = np.count_nonzero(np.logical_and(like, ~recommended))
print(f'FN@{K} = {FN}')
FP = np.count_nonzero(np.logical_and(~like, recommended))
print(f'FP@{K} = {FP}')
TN = np.count_nonzero(np.logical_and(~like, ~recommended))
print(f'TN@{K} = {TN}')
return TP, FN, FP, TN
u = 0
K = 3
TP, FN, FP, TN = confusion_matrix(u, RA, K)
print(f'混同行列 = \n{np.array([[TP, FN], [FP, TN]])}')
TPR = TP / (TP + FN)
print(f'TPR@{K} = {TPR:.3f}')
FPR = FP / (FP + TN)
print(f'FPR@{K} = {FPR:.3f}')
precision = TP / (TP + FP)
print(f'precision@{K} = {precision:.3f}')
recall = TP / (TP + FN)
print(f'recall@{K} = {recall:.3f}')
F1 = 2 * (precision * recall) / (precision + recall)
print(f'F1@{K} = {F1:.3f}')
import math
import numpy as np
np.set_printoptions(precision=3)
# 上位K件
TOP_K = 5
# 対数の底
ALPHA = 2
# テストデータ
R = np.array([
[5, 4, 3, np.nan, 5, 4, 2, 2, np.nan, np.nan],
[3, 3, 3, 3, 2, np.nan, 4, np.nan, 5, np.nan],
[4, np.nan, 3, 5, 4, 3, np.nan, 3, np.nan, np.nan],
])
U = np.arange(R.shape[0])
I = np.arange(R.shape[1])
Iu = [I[~np.isnan(R)[u,:]] for u in U]
# 推薦システムAによる推薦リスト
RA = np.array([
[1, np.nan, 3, np.nan, 4, 2, 5, np.nan, np.nan, np.nan],
[4, 1, np.nan, 3, np.nan, np.nan, 5, np.nan, 2, np.nan],
[np.nan, np.nan, 5, 3, 4, 2, np.nan, 1, np.nan, np.nan],
])
def confusion_matrix(u, RS, K):
"""
ユーザu向け推薦リストRSの上位K件における混同行列の各値を返す。
Parameters
----------
u : int
ユーザuのID
RS : ndarray
推薦リストRS
K : int
上位K件
Returns
-------
int
TP
int
FN
int
FP
int
TN
"""
like = R[u,Iu[u]]>=4
recommended = RS[u,Iu[u]]<=K
TP = np.count_nonzero(np.logical_and(like, recommended))
FN = np.count_nonzero(np.logical_and(like, ~recommended))
FP = np.count_nonzero(np.logical_and(~like, recommended))
TN = np.count_nonzero(np.logical_and(~like, ~recommended))
return TP, FN, FP, TN
R
の各要素が4以上ならばTrue
を返すような比較を行う。今回は単純に>=
による比較を行った。np.nanmin(RA[0, R[0] >= 4])
like
がTrue
であるアイテムの中で、対応するRA
の値が最も小さいものをとってくればよい。そのため、RA
のu
行目かつその成分に対応するlike
の行がTrue
になっているものをインデキシングで参照できるようにする。また、np.nan
があるので最小値を得るためにはnp.nanmin()
を用いる。ku
の各成分の逆数をとったリストの和を分子、ユーザーの数を分母とした値をMRR
に格納すればよい。u = 0
like = R >= 4
print(f'like = \n{like}')
ku = np.array([np.nanmin(RA[u, like[u]]) for u in U])
print(f'ku = {ku}')
MRR = np.array([1 / k for k in ku]).sum() / U.size
print(f'MRR = {MRR:.3f}')
u
の評価情報であるR
のu
行目について、RA
の昇順に並べ替えるためのインデックスをnp.argsort()
によって取得する。ranked_R
に対して、>=
による比較の結果を得ればよい。ranked_like
に対して、np.where(ranked_like, 1, 0)
とすれば望みの結果を得ることができる。APu
を作成する。そのために、06で作ったrel
のu
行目に対してインデックスTOP_K
までスライシングしたものと、precisions
のu
行目を同じくインデックスTOP_K
までスライシングしたものを用意する。これらの用意ができたら、の定義にある計算を行えばよい。# 各順位における適合率
precisions = []
for u in U:
precisions_u = []
for k in range(1, Iu[u].size+1):
TP, FN, FP, TN = confusion_matrix(u, RA, k)
precision_uk = TP / (TP + FP)
precisions_u.append(precision_uk)
precisions.append(precisions_u)
print(f'precisions = \n{precisions}')
ranked_R = np.array([R[u][np.argsort(RA[u])] for u in U])
print(f'ranked_R = \n{ranked_R}')
ranked_like = ranked_R >= 4
print(f'ranked_like = \n{ranked_like}')
rel = np.where(ranked_like, 1, 0)
print(f'rel = \n{rel}')
APu = np.array([np.dot(rel[u][:TOP_K], precisions[u][:TOP_K]) / rel[u][:TOP_K].sum() for u in U])
print(f'APu = {APu}')
MAP = APu.sum() / APu.size
print(f'MAP = {MAP:.3f}')
I
ではなくIu_rec
を使うこと。argsort()
を2重に使うことでRI
が計算可能である。注意点は、降順なので符号を逆にして引数に指定する必要があること。正直なところ仕組みがよくわからなかったので、以下のサイトを参照して方法だけわかったという感じ。RI
のu
行目についてTOP_K
以下であるような成分のインデックスをとってI
にインデキシングをすれば望みの結果であるIu_recI[u]
を得ることができる。DCGu
を計算したときの処理とほとんど同じ処理をすればよい。ただし、分母で用いる順位k_i
に相当するものは、理想的な順位であるRI
からとってくる必要がある。nDCGu
の平均をとればよい。Iu_rec = [I[~np.isnan(RA[u])] for u in U]
DCGu = np.array([sum([R[u, i] / max(1, math.log2(RA[u, i])) for i in Iu_rec[u]]) for u in U])
print(f'DCGu = {DCGu}')
RI = np.array([np.argsort(np.argsort(-R[u])) + 1 for u in U])
print(f'RI = \n{RI}')
Iu_recI = np.array([I[RI[u] <= TOP_K] for u in U])
print(f'Iu_recI = \n{Iu_recI}')
IDCGu = np.array([sum([R[u, i] / max(1, math.log2(RI[u, i])) for i in Iu_recI[u]]) for u in U])
print('IDCGu = {}'.format(IDCGu))
nDCGu = DCGu / IDCGu
print('nDCGu = {}'.format(nDCGu))
nDCG = nDCGu.sum() / nDCGu.size
print('nDCG = {:.3f}'.format(nDCG))