

import numpy as npnp.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になるようなベクトルを使ってあげればよいはず。
xnp.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() + ALPHAN*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 probALPHAだけは足すようにする。
# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
k = 1
i = 2
Du[:, k][(Du[i, k] == Du[:, k])&(ru == r)].size + ALPHAALPHAと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 probscoreを完成させる。やることとしては問題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, TNRの各要素が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))