Ccmmutty logo
Commutty IT
64 min read

recsys-pythonをやったやつまとめ

https://cdn.magicode.io/media/notebox/cc04ea9a-3ab8-44e9-ba92-c67fad59623f.jpeg

recsys-python

推薦システムを作ることで、機械学習を学ぶ。

第1章 評価履歴

準備
python
import numpy as np

評価履歴

01 評価履歴の生成

  1. np.nanを使う。
  2. np.arrayを使う。
python
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)
python
print(f'Du = \n{Du}')

Du = [[ 5. 3. 1.] [ 6. 2. 1.] [ 4. 1. 1.] [ 8. 5. -1.] [ 2. 4. -1.] [ 3. 6. -1.] [ 7. 6. -1.] [ 4. 2. nan] [ 5. 1. nan] [ 8. 6. nan] [ 3. 4. nan] [ 4. 7. nan] [ 4. 4. nan]]

02 評価履歴の形状

  • np.shapeを使う。
python
print(f'Duの形状 = {Du.shape}')

Duの形状 = (13, 3)

03 評価履歴の行数

  • np.shapeを使う。
python
print(f'Duの行数 = {Du.shape[0]}')

Duの行数 = 13

04 評価履歴の列数

  • np.shapeを使う。
python
print(f'Duの列数 = {Du.shape[1]}')

Duの列数 = 3

05 評価履歴の全要素数

  • np.sizeを使う。
    • おそらく、np.shape[0]*np.shape[1]でもよい。
python
print(f'Duの全要素数 = {Du.size}')

Duの全要素数 = 39

アイテム

06 アイテム集合

アイテムは、インデックスと読み替えてもこの場合差し支えなさそう。
python
I = np.arange(Du.shape[0])
print(f'I = {I}')

I = [ 0 1 2 3 4 5 6 7 8 9 10 11 12]

07 アイテムの特徴ベクトルの集合

numpy行列中にある評価値以外を取り出す。
python
x = Du[:, :-1]
print(f'x = \n{x}')

x = [[5. 3.] [6. 2.] [4. 1.] [8. 5.] [2. 4.] [3. 6.] [7. 6.] [4. 2.] [5. 1.] [8. 6.] [3. 4.] [4. 7.] [4. 4.]]

08 アイテムiiの特徴ベクトル

python
i = 0
print(f'x{i} = {x[i]}')

x0 = [5. 3.]

09 評価値集合

python
ru = Du[:, -1]
print(f'ru = {ru}')

ru = [ 1. 1. 1. -1. -1. -1. -1. nan nan nan nan nan nan]

10 評価値集合の形状

python
print(f'ruの形状 = {ru.shape}')

ruの形状 = (13,)

11 評価値集合の全要素数

python
print(f'ruの全要素数 = {ru.size}')

ruの全要素数 = 13

12 評価値集合の部分集合

python
i = 2
j = 5
print(f'ru{i}からru{j-1}までの評価値 = {ru[i:j]}')

ru2からru4までの評価値 = [ 1. -1. -1.]

13 評価値集合の逆順

ru[::-1]により、逆順で値を取り出す。
python
print(f'ruの逆順 = {ru[::-1]}')

ruの逆順 = [nan nan nan nan nan nan -1. -1. -1. -1. 1. 1. 1.]

14 アイテムiiに対する評価

ru[i]を使う。
python
i = 0
print(f'ru{i} = {ru[i]}')

ru0 = 1.0

アイテム集合

15 ユーザuuが未評価であるか否かの判定

ここでは、評価値rur_uについて、その第ii成分がnp.nanであるときユーザーuuがアイテムiiを未評価であるとして問題を解く。
np.isnan(nparray)を使う。
python
print(f'ユーザuが未評価 = {np.isnan(ru)}')

ユーザuが未評価 = [False False False False False False False True True True True True True]

16 ユーザuuが評価済みであるか否かの判定

np.isnan(nparray)に対して、~をつけてあげればTrueFalseが反転するので題意を満たす。
python
print(f'ユーザuが評価済み = {~np.isnan(ru)}')

ユーザuが評価済み = [ True True True True True True True False False False False False False]

17 ユーザuuが評価済みのアイテム集合IuI_u

16で見た評価済みのアイテムのみを抽出する。
np.arange()について、16の結果を使ってインデキシングすることで解決する。
python
Iu = np.arange(ru.shape[0])[~np.isnan(ru)]
print(f'Iu = {Iu}')

Iu = [0 1 2 3 4 5 6]

18 ユーザuuが好きと評価したアイテム集合Iu+I_{u+}

ユーザuuが評価済みで、なおかつ評価値が11であるようなアイテム集合IuI_uを作る。
np.where(ruの条件, True, False)を使う。
python
Iuplus = np.arange(ru.shape[0])[np.where(ru == 1, True, False)]
print(f'Iu+ = {Iuplus}')

Iu+ = [0 1 2]

19 ユーザuuが嫌いと評価したアイテム集合IuI_{u-}

np.where()の条件部分をいじればよい。
python
Iuminus = np.arange(ru.shape[0])[np.where(ru == -1, True, False)]
print(f'Iu- = {Iuminus}')

Iu- = [3 4 5 6]

20 ユーザuuが未評価のアイテム集合Iˉu\bar{I}_u

  1. np.nanかどうかでインデキシングする。
  2. numpy.setdiff1d()については、また今度。
python
Iu_not = np.arange(ru.shape[0])[np.isnan(ru)]
print(f'Iu_not = {Iu_not}')

Iu_not = [ 7 8 9 10 11 12]

訓練データと予測対象データ

21 訓練データ

17でやったインデキシングをDuに直接適用すればできるはず。
python
DuL = Du[~np.isnan(ru)]
print(f'DuL = \n{DuL}')

DuL = [[ 5. 3. 1.] [ 6. 2. 1.] [ 4. 1. 1.] [ 8. 5. -1.] [ 2. 4. -1.] [ 3. 6. -1.] [ 7. 6. -1.]]

22 訓練事例数

np.shapeDuLの行数を求めればよい。
python
print(f'|DuL| = {DuL.shape[0]}')

|DuL| = 7

23 正事例数

np.where(ruの条件, True, False)の長さは13なので、これでインデキシングしたい場合DuLを対象としてもエラーが発生する。(一敗)
そのため今回はDuL[:, -1]、つまりDuLの中でも評価値の部分に対してnp.whereを利用した。
python
print(f'|DuL+| = {DuL[np.where(DuL[:, -1] == 1, True, False)].shape[0]}')

|DuL+| = 3

24 不事例数

python
print(f'|DuL-| = {DuL[np.where(DuL[:, -1] == -1, True, False)].shape[0]}')

|DuL-| = 4

25 予測対象データ

21でやったこととほぼ同じことをやればよい。~を外すだけ。
python
DuU = Du[np.isnan(ru)]
print(f'DuU = \n{DuU}')

DuU = [[ 4. 2. nan] [ 5. 1. nan] [ 8. 6. nan] [ 3. 4. nan] [ 4. 7. nan] [ 4. 4. nan]]

26 予測対象事例数

対象をDuUとして、22と同じことを行う。
python
print(f'|DuU| = {DuU.shape[0]}')

|DuU| = 6

第2章 評価値行列

準備
python
import pprint
import numpy as np
np.set_printoptions(precision=3)

評価値行列

01 評価値行列の生成

正確に内容をトレースし、np.nannp.arrayを使う。
python
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)
python
print(f'R = \n{R}')

R = [[nan 4. 3. 1. 2. nan] [ 5. 5. 4. nan 3. 3.] [ 4. nan 5. 3. 2. nan] [nan 3. nan 2. 1. 1.] [ 2. 1. 2. 4. nan 3.]]

02 ユーザ集合

第1章で親の顔ほど見たnp.arange(np.shape[0])を使う。
python
U = np.arange(R.shape[0])
print(f'U = {U}')

U = [0 1 2 3 4]

03 アイテム集合

02と同様に、np.arange(np.shape[1])でなんとかする。
python
I = np.arange(R.shape[1])
print(f'I = {I}')

I = [0 1 2 3 4 5]

04 ユーザ数

公式のヒント的にUに対してnp.sizeを使うらしい。行列ではなくベクトルの場合だとnp.shapeとの違いがいまいちわからなかったりするので調べておこうと思う。
python
print(f'|U| = {U.size}')

|U| = 5

05 アイテム数

04と同じように書く。
python
print(f'|I| = {I.size}')

|I| = 6

06 評価値

uuiiは与えてもらえるので、インデックスを指定して値を取り出す。
python
u = 0
i = 1
print(f'r{u}{i} = {R[u, i]}')

r01 = 4.0

評価値行列の疎性

07 評価値行列の全要素数

第1章では同じような状況でnp.sizeを使っていた。今回は、np.shape[0]*np.shape[1]で値を取り出してみる。
python
print(f'Rの全要素数 = {R.shape[0]*R.shape[1]}')

Rの全要素数 = 30

08 観測されているか否かの判定

np.isnan(R)によりRの各成分が欠損値かどうかを確認する。今回の場合は観測されているならばTrueとするようなので別途~を付け加える必要がある。
python
print(f'観測値 = \n{~np.isnan(R)}')

観測値 = [[False True True True True False] [ True True True False True True] [ True False True True True False] [False True False True True True] [ True True True True False True]]

09 評価値行列の観測値数

np.count.nonzero()を使えばよいらしい。そのまま真偽値のみで構成された行列に適用すると、Trueの数を数えてくれるようだ。
python
print(f'|R| = {np.count_nonzero(~np.isnan(R))}')

|R| = 22

10 評価値行列の疎性

sparsitysparsityの定義に基づいて計算すればよい。少しコードが冗長になると思ったので、R.shape[index]ではなくR.sizeを使って計算している。
python
sparsity = (R.size-np.count_nonzero(~np.isnan(R)))/R.size
print(f'sparsity = {sparsity:.3f}')

sparsity = 0.267

評価済みアイテム集合

11 ユーザuuが評価済みのアイテム集合IuI_u

uuが与えられるので、Ru行目の中で欠損値ではないインデックスのみを返すようにする。
python
u = 0
print(f'I{u} = {I[~np.isnan(R[u, :])]}')

I0 = [1 2 3 4]

12 各ユーザの評価済みアイテム集合

ポイントは、多分以下だと思う。
  • ndarrayのリストとしてまとめるという点
    • リストにする際に内包表記を利用する点
  • pprintを利用する点
python
Iu = [I[~np.isnan(R[u, :])] for u in range(U.size)]
print('Iu = ')
pprint.pprint(Iu)

Iu = [array([1, 2, 3, 4]), array([0, 1, 2, 4, 5]), array([0, 2, 3, 4]), array([1, 3, 4, 5]), array([0, 1, 2, 3, 5])]

13 ユーザuuvvの共通の評価済みアイテム集合Iu,vI_{u, v}

uuvvが与えられるので、11のように評価済みアイテムを得てその共通部分をとることで望みのベクトルが得られる。共通部分の取り方は、案内通りnp.intersect1d(ndarray1, ndarray2)を使えばよい。
python
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}')

I01 = [1 2 4]

14 アイテムiiを評価済みのユーザ集合

iiが与えられるので、Uからii列目がnp.nanでないインデックスを返すように~np.isnan()を使う。
python
i = 0
print(f'U{i} = {U[~np.isnan(R[:, i])]}')

U0 = [1 2 4]

15 各アイテムの評価済みユーザ集合

11のコードを参考に12のコードを作ったように、14のコードを修正する。
python
Ui = [U[~np.isnan(R[:, i])] for i in range(I.size)]
print('Ui = ')
pprint.pprint(Ui)

Ui = [array([1, 2, 4]), array([0, 1, 3, 4]), array([0, 1, 2, 4]), array([0, 2, 3, 4]), array([0, 1, 2, 3]), array([1, 3, 4])]

16 アイテムiiとアイテムjjの両方を評価済みのユーザ集合Ui,jU_{i, j}

13と同じノリで作ることができる。
python
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}')

U04 = [1 2]

17 評価値行列全体の平均評価値

まずは、何も考えずにRnp.mean()で平均することを考えたい。すると、悲しいことに以下のようになるのでnp.nanを避ける必要がある。
python
np.mean(R)

nan
ここで、元ネタの記事にもある通りnp.nanmean()をすることでnp.nanを除外した平均を算出できる。
python
print(f'R全体の平均評価値 = {np.nanmean(R):.3f}')

R全体の平均評価値 = 2.864

18 各アイテムの平均評価値

便利だったnp.nanmean()np.mean()と同様にaxisを設定できるのでそれを素直に行うことで結果を得る。この場合axis=0を指定する必要がある。
python
ri_mean = np.nanmean(R, axis=0)
print(f'ri_mean = {ri_mean}')

ri_mean = [3.667 3.25 3.5 2.5 2. 2.333]

19 各ユーザの平均評価値

18の、axis=1版を作ればよい。
python
ru_mean = np.nanmean(R, axis=1)
print(f'ru_mean = {ru_mean}')

ru_mean = [2.5 4. 3.5 1.75 2.4 ]

20 評価値ベクトルの形状変換

記載がある通り、ndarray.reshape()を使うことで望みの結果を得る。今回はru_meanを縦ベクトルのような表示にすることが目標となっていて、reshape(-1, 1)とするのがいいらしいが紛らわしいと思ってしまった。
python
print(f'ru_mean = \n{ru_mean.reshape(-1, 1)}')

ru_mean = [[2.5 ] [4. ] [3.5 ] [1.75] [2.4 ]]

21 平均中心化評価値行列

R - ru_meanは計算できず、エラーが発生する。ブロードキャストによって計算できそうだと期待してやってみた結果これがでたのでちょっとつらいが内包表記によってRとの差をとるような計算を試みた。理想的な解答とはかなり乖離していると思われるので、あとで調べてメモっておく。
以下、内包表記を利用した計算。一応値は合っているのだがndarray.reshape()を使う方法を思いつかなかったのが悲しい。
python
rubar = np.array([[rm] * I.size for rm in ru_mean])
R2 = R - rubar
print(f'R\' = \n{R2}')

R' = [[ nan 1.5 0.5 -1.5 -0.5 nan] [ 1. 1. 0. nan -1. -1. ] [ 0.5 nan 1.5 -0.5 -1.5 nan] [ nan 1.25 nan 0.25 -0.75 -0.75] [-0.4 -1.4 -0.4 1.6 nan 0.6 ]]
以下、第4章と同時に追記
ru_meanではなくru_mean.reshape(-1, 1)を使えばRとの差が直接計算できるということに後で気づいたのでコードを次のセルに記載する。
python
R2 = R - ru_mean.reshape(-1, 1)
print(f'R\' = \n{R2}')

R' = [[ nan 1.5 0.5 -1.5 -0.5 nan] [ 1. 1. 0. nan -1. -1. ] [ 0.5 nan 1.5 -0.5 -1.5 nan] [ nan 1.25 nan 0.25 -0.75 -0.75] [-0.4 -1.4 -0.4 1.6 nan 0.6 ]]

第3章 類似度に基づく推薦

準備
python
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)

ユーザプロファイル

01 好きなアイテム集合に含まれるアイテムの特徴ベクトルの集合

xは以下の通りであるから、ru1となっているインデックスのみがTrueになるようなベクトルを使ってあげればよいはず。
python
x

array([[5., 3.], [6., 2.], [4., 1.], [8., 5.], [2., 4.], [3., 6.], [7., 6.], [4., 2.], [5., 1.], [8., 6.], [3., 4.], [4., 7.], [4., 4.]])
従って、以下のように書くことで答えを得た。np.where()を用いることによって、ruの値が1のところのみTrueになるようにしている。
python
print(f'x[Iu+] = \n{x[np.where(ru == 1, True, False)]}')

x[Iu+] = [[5. 3.] [6. 2.] [4. 1.]]

02 特徴ベクトルの総和

これは、01で得たIu+に対して、axisを工夫して総和をとればよいはずだ。今回は行方向に和をとるはずだから、axis=0を使えばいいかもしれない。
python
print(f'sum(x[Iu+]) = {x[np.where(ru == 1, True, False)].sum(axis=0)}')

sum(x[Iu+]) = [15. 6.]

03 ユーザプロファイル

02のコードについて、総和ではなく平均をとるように修正すればよい。
python
pu = x[np.where(ru == 1, True, False)].mean(axis=0)
print(f'pu = {pu}')

pu = [5. 2.]

コサイン類似度

04 ベクトルの内積

ここでは、04から06までの3問を解いて一部コードを与えられた関数cosを完成させることを考える。
内積numは、よく用いられる@np.dot()を利用して計算されるので今回もそれを用いて計算していく。

05 ユーザプロファイルのノルム

ユーザプロファイルのノルムden_uは、np.linalg.norm()をユーザプロファイルのベクトルに使うことで計算可能なはずだ。引数ordは何も設定しない場合デフォルトで2になったような気もするが、一応指定はしておく。

06 特徴ベクトルのノルム

05のコードに対して、np.linalg.norm()の引数に指定するベクトルをxiに変更すればよい。
以下では、04から06までの問の答えを用意してcos関数を作成し、実行結果を確認している。
python
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
python
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}')

num = 24.0 den_u = 5.385 den_i = 4.472 cos(p0, x7) = 0.997 num = 34.0 den_u = 5.385 den_i = 8.062 cos(p0, x11) = 0.783

推薦

07 各アイテムに対するスコア

ここでは、07と08の問題を解いて一部コードを与えられた関数orderを完成させることを考える。
結果から、変数scoresは、以下のような条件を満たす辞書型の変数となる。
  • キーはnp.nanだったアイテムのインデックス
    • Iu_notがあるのでこれを用いる
  • バリューはキーのアイテムベクトルとユーザプロファイルのスコアをとったもの
これらを満たすよう、今回は辞書の内包表記を用いて記述する。

08 推薦リスト

単純に辞書型の変数scoresをバリューによって並び替えればよい。引数reverseTrueにすることで降順にする点もポイント。
以下では、07から08までの問の答えを用意してorder関数を作成し、実行結果を確認している。cos関数をそのまま実行するとprint文が実行されてしまうので、cos2関数を用意してプリントされないようにしている。
python
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
python
def score(u, i):
    """
    スコア関数:ユーザuのアイテムiに対するスコアを返す。

    Parameters
    ----------
    u : int
        ユーザuのID(ダミー)
    i : int
        アイテムiのID

    Returns
    -------
    float
        スコア
    """
    return cos2(pu, x[i])
python
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
python
u = 0
rec_list = order(u, Iu_not)
print('rec_list = ')
for i, scr in rec_list:
    print('{}: {:.3f}'.format(i, scr))

scores = {7: 0.9965457582448796, 8: 0.9832820049844603, 9: 0.9656157585206697, 10: 0.8541985556144386, 11: 0.783110847498294, 12: 0.9191450300180578} rec_list = 7: 0.997 8: 0.983 9: 0.966

第4章 kk近傍法

準備
python
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)

距離

01 ユークリッド距離

この問題では、一部コードを与えられた関数distを完成させる。distでは2つのベクトルを入力としてそのユークリッド距離を返すものなので、2つのベクトルの差に対してnp.linalg.normを使えばよいはずだ。
python
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
python
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}')

dist(x7, x2) = 1.000 dist(x7, x3) = 5.000

近傍アイテム

02 アイテム-アイテム距離行列

IuIu_notの両方からアイテムを取り出し、アイテムの持つベクトルの距離をdist関数を使って求める問題である。とりあえずnp.array()により2重内包表記の結果を包むことで答えと同じものが返ってくるようにはしたけど、題意的には正解と言いがたいかもしれない。あと、正直D[np.ix_(Iu_not,Iu)]がよくわかっていない。
python
D = np.array([[dist(x[i], x[j]) for i in Iu] for j in Iu_not])
print(f'D = \n{D}')

D = [[1.414 2. 1. 5. 2.828 4.123 5. ] [2. 1.414 1. 5. 4.243 5.385 5.385] [4.243 4.472 6.403 1. 6.325 5. 1. ] [2.236 3.606 3.162 5.099 1. 2. 4.472] [4.123 5.385 6. 4.472 3.606 1.414 3.162] [1.414 2.828 3. 4.123 2. 2.236 3.606]]

03 距離の昇順に並べ替えたインデックスの配列

xの各行xindexIuに含まれる各アイテムiuの持つベクトルx[iu]の距離を求めて02と同じようにndarrayに変換し、argsortによって昇順に番号をつけることで望みの結果を得た。
python
Ii = np.array([[dist(xindex, x[iu]) for iu in Iu] for xindex in x]).argsort()
print(f'Ii = \n{Ii}')

Ii = [[0 1 2 4 3 5 6] [1 0 2 3 6 4 5] [2 0 1 4 5 3 6] [3 6 0 1 5 2 4] [4 5 0 2 1 6 3] [5 4 0 6 1 2 3] [6 3 0 5 1 4 2] [2 0 1 4 5 3 6] [2 1 0 4 3 5 6] [3 6 0 1 5 4 2] [4 5 0 2 1 6 3] [5 6 4 0 3 1 2] [0 4 5 1 2 6 3]]

04 近傍kk件のアイテムのインデックス配列

Iiの結果に対して、スライシングすることで最初のTOP_K列分だけを抽出する。これを使って評価値が+1だったアイテムと近いアイテムがどれであったかを確認することができる。各行の持つ数値の中に3以上のものがあったのならば、「嫌い」と評価したアイテムが最近傍の3件に含まれていることを示している。
python
Ii = Ii[:, :TOP_K]
print(f'Ii = \n{Ii}')

Ii = [[0 1 2] [1 0 2] [2 0 1] [3 6 0] [4 5 0] [5 4 0] [6 3 0] [2 0 1] [2 1 0] [3 6 0] [4 5 0] [5 6 4] [0 4 5]]

05 各対象アイテムの近傍アイテム集合

Iu_notからアイテムをとってきて、保存したIiからアイテムで行を指定して抽出することを考える。
python
Ii = {i: Ii[i] for i in Iu_not}
print('Ii = ')
pprint.pprint(Ii)

Ii = {7: array([2, 0, 1]), 8: array([2, 1, 0]), 9: array([3, 6, 0]), 10: array([4, 5, 0]), 11: array([5, 6, 4]), 12: array([0, 4, 5])}

嗜好予測(多数決方式)

06 近傍アイテム集合のうち「好き」と評価したアイテム集合

ここでは、06から08の問題を解いて一部コードを与えられた関数predict1を完成させる。
06では、Ii[i]np.isin()を使ったインデキシングをすることで答えを得る。Ii[i]はアイテム集合のうち、距離が近かったアイテム3件の中に「好き」と評価したアイテムが入っているかどうかを判断するための数値が入っている。

07 近傍アイテム集合のうち「嫌い」と評価したアイテム集合

07では、06と同様の方法で答えを得ることができる。

08 多数決方式による予測評価値

IipIinに対してnp.size()を使うことでiiの近傍アイテム集合中に「好き」と評価したアイテムがいくつあるか、「嫌い」と評価したアイテムがいくつあるかがわかるので、その結果に対して素直にif文を使ってあげればよい。
python
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
python
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}')

I7+ = [2 0 1] I7- = [] predict1(0, 7) = 1.000 I9+ = [0] I9- = [3 6] predict1(0, 9) = -1.000

嗜好予測(平均方式)

09 平均方式による予測評価値

06から08で完成させた関数predict1を参考に、ruiの部分を平均方式に修正したものを関数predict2として作成する。
python
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
python
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}')

I7+ = [2 0 1] I7- = [] predict2(0, 7) = 1.000 I9+ = [0] I9- = [3 6] predict2(0, 9) = -1.000

推薦

10 推薦リスト

ここでは、問10を解いて一部コードを与えられた関数orderを完成させる。そのために、scoresに格納された値score(u, i)THETA未満になるようなペアを除くための工夫を考える。
問題の指定からscorepredict2関数で作るものとして、scoresの部分をいじる必要がある。ここでは辞書内包表記の中でif文を用いることによってTHETA未満の値をすべて削除することを考える。
python
def score(u, i):
    """
    スコア関数:ユーザuのアイテムiに対するスコアを返す。

    Parameters
    ----------
    u : int
        ユーザuのID
    i : int
        アイテムiのID

    Returns
    -------
    float
        スコア
    """
    return predict2(u, i)
python
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
python
u = 0
rec_list = order(u, Iu_not)
print('rec_list = ')
for i, scr in rec_list:
    print('{}: {:.3f}'.format(i, scr))

rec_list = 7: 1.000 8: 1.000

第5章 ユーザベース協調フィルタリング

準備
python
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))

ピアソンの相関係数

01 ピアソンの相関係数(分子)

以下では、01から03の問題を解いて一部コードを与えられた関数pearson1を完成させる。
01はピアソンの相関係数の分子を、取得した各ベクトルから計算するというものである。Iuvは、ユーザuuやユーザvvによる評価値が存在する列のみを集めたIu[u]Iu[v]の共通項をとることで比較可能なアイテムの評価を取得したものである。これに対し、各ユーザの評価を平均したものを作って差をとり、内積を計算すればよい。

02 ピアソンの相関係数の算出(分母左部)

内積を作る際に作ったユーザuuに関するベクトルをruiとして、それに対してnp.linalg.norm(uri, ord = 2)を使ってあげればよい。

03 ピアソンの相関係数の算出(分母右部)

内積を作る際に作ったユーザvvに関するベクトルをrviとして、それに対してnp.linalg.norm(uvi, ord = 2)を使ってあげればよい。
python
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
python
u = 0
v = 1
prsn = pearson1(u, v)
print(f'pearson1({u}, {v}) = {prsn:.3f}')

num = 2.0 den_u = 1.658 den_v = 1.414 pearson1(0, 1) = 0.853

平均中心化評価値行列に基づくピアソンの相関係数

04 ピアソンの相関係数(分子)

以下では、04から06の問題を解いて一部コードを与えられた関数pearson2を完成させる。
04は01と同じようにピアソンの相関係数の分子を、取得した各ベクトルから計算するというものである。RでなくR2を利用するところが大きな変更点となる。

05 ピアソンの相関係数の算出(分母左部)

内積を作る際に作ったユーザuuに関するベクトルをruiとして、それに対してnp.linalg.norm(uri, ord = 2)を使ってあげればよい。02と違い、R2を使う点がポイントとなっている。

06 ピアソンの相関係数の算出(分母右部)

03と同様に内積を作る際に作ったユーザvvに関するベクトルをrviとして、それに対してnp.linalg.norm(uvi, ord = 2)を使ってあげればよい。R2を使う点がポイント。
python
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
python
u = 0
v = 1
prsn = pearson2(u, v)
print(f'pearson2({u}, {v}) = {prsn:.3f}')

num = 2.0 den_u = 1.658 den_v = 1.414 pearson2(0, 1) = 0.853

ユーザ-ユーザ類似度行列

07 ユーザ-ユーザ類似度行列

ユーザ対ユーザについてその相関係数をR2によって計算するという問題。内包表記で2重リストを作って、np.array()すればよさそう。以下では、それらに加えて関数simで利用するのが関数pearson2だと各種printが発生する問題があったのでそれを取り除いた関数pearson3を別途作って対応した。
python
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
python
def sim(u, v):
    """
    ユーザ類似度関数:ユーザuとユーザvのユーザ類似度を返す。

    Parameters
    ----------
    u : int
        ユーザuのID
    v : int
        ユーザvのID

    Returns
    -------
    float
        ユーザ類似度
    """
    return pearson3(u, v)
python
S = np.array([[sim (u, v) for u in U] for v in U])
print(f'S = \n{S}')

S = [[ 1. 0.853 0.623 0.582 -0.997] [ 0.853 1. 0.649 0.968 -0.853] [ 0.623 0.649 1. 0.8 -0.569] [ 0.582 0.968 0.8 1. -0.551] [-0.997 -0.853 -0.569 -0.551 1. ]]

類似ユーザの選定

08 類似度上位k人のユーザ集合

以下では、08と09の問題を解いて一部コードを与えられた処理について完成させる。08ではあらかじめユーザの類似度を与えた行列Sから作った辞書が与えられていて、キーはユーザ、バリューはまた別の辞書となっていることがUuからわかる。
08では、Uuのバリューの中で各キーに対して格納されている辞書を類似度の順番に並べて上位K_USERS件のみを残すというものであるため、その辞書のソートを行う。

09 類似度がしきい値以上のユーザ集合

09ではUuのバリュー側に格納された辞書について、条件式を使ってそのバリューの値がTHETA未満のものを除外する。
python
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)

Uu = {0: {1: 0.8528028654224417, 2: 0.6225430174794672, 3: 0.5816750507471109, 4: -0.9968461286620518}, 1: {0: 0.8528028654224417, 2: 0.6488856845230501, 3: 0.9684959969581863, 4: -0.8528028654224418}, 2: {0: 0.6225430174794672, 1: 0.6488856845230501, 3: 0.7999999999999998, 4: -0.5685352436149611}, 3: {0: 0.5816750507471109, 1: 0.9684959969581863, 2: 0.7999999999999998, 4: -0.550920031004556}, 4: {0: -0.9968461286620518, 1: -0.8528028654224418, 2: -0.5685352436149611, 3: -0.550920031004556}} Uu = {0: {1: 0.8528028654224417, 2: 0.6225430174794672, 3: 0.5816750507471109}, 1: {0: 0.8528028654224417, 2: 0.6488856845230501, 3: 0.9684959969581863}, 2: {0: 0.6225430174794672, 1: 0.6488856845230501, 3: 0.7999999999999998}, 3: {0: 0.5816750507471109, 1: 0.9684959969581863, 2: 0.7999999999999998}, 4: {1: -0.8528028654224418, 2: -0.5685352436149611, 3: -0.550920031004556}} Uu = {0: {1: 0.8528028654224417, 2: 0.6225430174794672, 3: 0.5816750507471109}, 1: {0: 0.8528028654224417, 2: 0.6488856845230501, 3: 0.9684959969581863}, 2: {0: 0.6225430174794672, 1: 0.6488856845230501, 3: 0.7999999999999998}, 3: {0: 0.5816750507471109, 1: 0.9684959969581863, 2: 0.7999999999999998}, 4: {}} Uu = {0: array([1, 2, 3]), 1: array([3, 0, 2]), 2: array([3, 1, 0]), 3: array([1, 2, 0]), 4: array([], dtype=float64)}

嗜好予測

10 類似ユーザ集合の中でアイテムiを評価済みのユーザ集合

以下では、10と11の問題を解いて一部コードを与えられた関数predictについて完成させる。
Uuiは、アイテムiを評価したユーザUi[i]とユーザuの近傍にいるユーザUu[u]の共通点を、np.intersect1d()を使って求めることで作ることができる。

11 予測評価値

11では、予測評価値のUuiの大きさが0でなかった場合に発生する複雑そうな項を作っていく。sim(u, v)S[u, v]であったので、uに対するUuiをインデックスに指定してあげてndarray状のベクトルを取得して計算に用いる。rv,ir^{'}_{v, i}についてはR2[v, i]が相当する部分だったはずなのでv部分をUuiで指定してあげて計算用のndarrayを取得する。こうして取得したベクトルを、前者のndarrayは分子の内積計算と分母の絶対値をとった和の計算に、後者のndarrayは分子の内積計算に用いればよい。
python
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
python
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)))

U00 = [1 2] r00 = 3.289 U05 = [1 3] r05 = 1.601

評価値行列の補完

12 評価値行列の補完

行列状のRに対して、np.nanの部分のみを予測値predict(u, i)で埋めてあげればよい。余計なprintが発生する関係で、ここではpredict12関数を別途用意した上で、2重内包表記の各成分を求めつつ最後にnp.array()ndarrayとしたR3を作った。
python
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
python
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}')

R'' = [[3.289 4. 3. 1. 2. 1.601] [5. 5. 4. 3.449 3. 3. ] [4. 4.747 5. 3. 2. 2.638] [2.524 3. 2.384 2. 1. 1. ] [2. 1. 2. 4. 2.4 3. ]]

第6章 アイテムベース協調フィルタリング

準備
python
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))

コサイン類似度

01 アイテムiiとアイテムjjのコサイン類似度

問題01では、一部コードのみ与えられた関数cosを完成させていく。することとしては、内積をとるためのベクトルをとるためのインデックスはUijに用意されているのでそれを用いてRからijのベクトルを取得することである。
python
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
python
i = 0
j = 4
cosine = cos(i, j)
print(f'cos({i}, {j}) = {cosine:.3f}')

cos(0, 4) = 0.996

調整コサイン類似度

02 アイテムiとアイテムjの調整コサイン類似度

問題02では、一部コードのみ与えられた関数adjusted_cosを完成させていく。することとしては、問題01で作った部分をRではなくR2を利用する形に書き直せばよい。
python
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
python
i = 0
j = 4
cosine = adjusted_cos(i, j)
print(f'adjusted_cos({i}, {j}) = {cosine:.3f}')

adjusted_cos(0, 4) = -0.868

アイテム-アイテム類似度行列

03 アイテム-アイテム類似度行列

問題03では、アイテム間の類似度行列SSを作成する。2重内包表記の各成分を、与えられた関数simによる計算によって与えればよい。
python
def sim(i, j):
    """
    アイテム類似度関数:アイテムiとアイテムjのアイテム類似度を返す。

    Parameters
    ----------
    i : int
        アイテムiのID
    j : int
        アイテムjのID

    Returns
    -------
    float
        アイテム類似度
    """
    return adjusted_cos(i, j)
python
S = np.array([[sim(i, j) for i in I] for j in I])
print('S = \n{}'.format(S))

S = [[ 1. 0.842 0.494 -0.829 -0.868 -0.987] [ 0.842 1. 0.896 -0.788 -0.91 -0.942] [ 0.494 0.896 1. -0.583 -0.845 -0.514] [-0.829 -0.788 -0.583 1. 0.469 0.497] [-0.868 -0.91 -0.845 0.469 1. 1. ] [-0.987 -0.942 -0.514 0.497 1. 1. ]]

類似アイテムの選定

04 類似度上位k件のアイテム集合

以下では、04と05の問題を解いて一部コードを与えられた処理について完成させる。04ではあらかじめユーザの類似度を与えた行列Sから作った辞書が与えられていて、キーはユーザ、バリューはまた別の辞書となっていることがIiからわかる。これに対して、少し複雑だが入れ子になっているほうの辞書のバリューによって並べ替えを行う。

05 類似度がしきい値以上のアイテム集合

05ではIiのバリュー側に格納された辞書について、条件式を使ってそのバリューの値がTHETA未満のものを除外する。
python
# アイテム-アイテム類似度行列から対象アイテムを除外した辞書
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)

Ii = {0: {1: 0.8418791389638738, 2: 0.49365474375598073, 3: -0.8291725540450335, 4: -0.8682431421244593, 5: -0.987241120712647}, 1: {0: 0.8418791389638738, 2: 0.896314672184623, 3: -0.7876958617794716, 4: -0.9099637547345425, 5: -0.9419581446623225}, 2: {0: 0.49365474375598073, 1: 0.896314672184623, 3: -0.5833076828172804, 4: -0.8451542547285166, 5: -0.5144957554275266}, 3: {0: -0.8291725540450335, 1: -0.7876958617794716, 2: -0.5833076828172804, 4: 0.4685212856658182, 5: 0.49665813370370504}, 4: {0: -0.8682431421244593, 1: -0.9099637547345425, 2: -0.8451542547285166, 3: 0.4685212856658182, 5: 1.0}, 5: {0: -0.987241120712647, 1: -0.9419581446623225, 2: -0.5144957554275266, 3: 0.49665813370370504, 4: 1.0}} Ii = {0: {1: 0.8418791389638738, 2: 0.49365474375598073, 3: -0.8291725540450335}, 1: {0: 0.8418791389638738, 2: 0.896314672184623, 3: -0.7876958617794716}, 2: {0: 0.49365474375598073, 1: 0.896314672184623, 5: -0.5144957554275266}, 3: {2: -0.5833076828172804, 4: 0.4685212856658182, 5: 0.49665813370370504}, 4: {2: -0.8451542547285166, 3: 0.4685212856658182, 5: 1.0}, 5: {2: -0.5144957554275266, 3: 0.49665813370370504, 4: 1.0}} Ii = {0: {1: 0.8418791389638738, 2: 0.49365474375598073}, 1: {0: 0.8418791389638738, 2: 0.896314672184623}, 2: {0: 0.49365474375598073, 1: 0.896314672184623}, 3: {4: 0.4685212856658182, 5: 0.49665813370370504}, 4: {3: 0.4685212856658182, 5: 1.0}, 5: {3: 0.49665813370370504, 4: 1.0}} Ii = {0: array([1, 2]), 1: array([2, 0]), 2: array([1, 0]), 3: array([5, 4]), 4: array([5, 3]), 5: array([4, 3])}

嗜好予測

06 類似アイテム集合の中でユーザuが評価値を与えているアイテム集合

Iiuは、ユーザuが評価したアイテムIu[u]とアイテムiiの近傍にあるアイテムIi[i]の共通点を、np.intersect1d()を使って求めることで作ることができる。

07 予測評価値

07では、予測評価値のIiuの大きさが0でなかった場合に発生する複雑そうな項を作っていく。やっていることは基本的に第5章の11に書いた操作で、対象の変数を変えた以外は同じと考えてもさしつかえないように思う。
python
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
python
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}')

I00 = [1 2] r00 = 3.630 I50 = [3 4] r05 = 1.668

第7章 評価履歴の次元削減

準備
python
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]

分散共分散行列

01 各特徴量の平均値

結果欄から、xの行方向つまりaxis=0について平均をとればよい。
python
xk_mean = x.mean(axis=0)
print('xk_mean = {}'.format(xk_mean))

xk_mean = [4.846 3.923 5. ]

02 各特徴量の分散

01と同様にvarで分散をとればよい。求める値は、数式から見ても不偏分散ではないことに注意。
python
s2 = x.var(axis=0)
print(f's^2 = {s2}')

s^2 = [3.361 3.763 4.769]

03 各特徴量の標準化

01と02の結果を用いて、標準化を行う。numpyのブロードキャストによって単純に差や商をとる計算が使えるので普通に計算すればよい。一応、np.sqrt()を使う必要があることに注意。
python
x2 = (x - xk_mean) / np.sqrt(s2)
print(f'x\' = \n{x2}')

x' = [[ 0.084 -0.476 -0.916] [ 0.629 -0.991 0. ] [-0.462 -1.507 0. ] [ 1.72 0.555 1.832] [-1.552 0.04 -1.374] [-1.007 1.071 0. ] [ 1.175 1.071 1.374] [-0.462 -0.991 -0.916] [ 0.084 -1.507 1.374] [ 1.72 1.071 0.458] [-1.007 0.04 -1.374] [-0.462 1.586 0. ] [-0.462 0.04 -0.458]]

04 標準化された特徴量kkと特徴量llの共分散

03で計算済みのx2を用いて共分散を求めるとよい。np.covの引数でbias=Trueを用いることで不偏分散ではない分散を求めることができる。
python
k = 0
l = 1
skl = np.cov(x2[:, k], x2[:, l], bias = True)[0, 1]
print(f's{k}{l} = {skl:.3f}')

s01 = 0.191

05 分散共分散行列

04でも使ったnp.covを使って分散共分散行列を求めればよい。ただ、np.covにそのままx2を入れると行対行の分散共分散行列が作られるのでx2.Tという転置したものを引数に指定する。
python
S = np.cov(x2.T, bias = True)
print(f'S = \n{S}')

S = [[1. 0.191 0.749] [0.191 1. 0.163] [0.749 0.163 1. ]]

固有値・固有ベクトル

06 固有値・固有ベクトル

問題の案内にもある通り、np.linalg.eig()を利用して固有値と固有ベクトルを求める。
python
lmd, v = LA.eig(S)
print(f'λ = {lmd}')
print(f'v = \n{v}')

λ = [1.826 0.25 0.924] v = [[-0.679 -0.71 0.186] [-0.291 0.028 -0.956] [-0.674 0.704 0.225]]

07 固有値の降順にソートしたインデックス配列

固有値が大きい順に並べた場合のインデックスを求める。そのためにnp.argsort()を用いればよいのだが、今回は降順なのでlmdを引数に指定する際に符号を逆にしてやる必要がある。
python
indices = np.argsort(-lmd)
print(f'indices = {indices}')

indices = [0 2 1]

08 固有値の降順に固有値配列をソート

lmdのインデキシングにindicesを使ってあげればよい。
python
lmd = lmd[indices]
print(f'λ = {lmd}')

λ = [1.826 0.924 0.25 ]

09 固有値の降順に固有ベクトル配列をソート

vの列方向について、indicesでその順番を指定してあげればよい。
python
v = v[:, indices]
print(f'v = \n{v}')

v = [[-0.679 0.186 -0.71 ] [-0.291 -0.956 0.028] [-0.674 0.225 0.704]]

10 第d主成分までの固有ベクトル

vDIM列めまでをスライシングすることによってVを作ればよい。
python
V = v[:, :DIM]
print(f'V = \n{V}')

V = [[-0.679 0.186] [-0.291 -0.956] [-0.674 0.225]]

主成分得点

11 アイテムiiの第kk主成分得点

数式の定義から、主成分得点はx2i行めとvk列目の内積で得られるのでその通りに計算する。
python
i = 0
k = 0
xik3 = np.dot(x2[i, :] ,v[:, k])
print(f'x{i}{k}\'\' = {xik3:.3f}')

x00'' = 0.699

12 各アイテムの次元削減後の特徴ベクトル

@演算子を使って、x2Vの行列計算を行えばよい。
python
x3 = x2 @ V
print(f'x\'\' = \n{x3}')

x'' = [[ 0.699 0.264] [-0.139 1.065] [ 0.752 1.355] [-2.564 0.202] [ 1.969 -0.636] [ 0.373 -1.211] [-2.035 -0.496] [ 1.219 0.656] [-0.545 1.766] [-1.788 -0.601] [ 1.598 -0.535] [-0.148 -1.603] [ 0.611 -0.227]]

寄与率

13 第kk主成分の寄与率

固有値の和の中で第kk主成分がどれだけの大きさを占めるかを、寄与率と呼んでいる。分散共分散行列はその性質上固有値が0未満にはならないので、分母が0になることもなく安心して割合の計算を行うことができたはず。
python
# k=0とすると第1主成分になる。おそらくインデックスの都合。
k = 0
pk = lmd[k]/lmd.sum()
print(f'第{k+1}主成分の寄与率 = {pk:.3f}')

第1主成分の寄与率 = 0.609

14 第k主成分までの累積寄与率

13を参考に、pkの分子の部分をスライスした部分の和としてあげればよい。
python
k = 2
ck = lmd[:k].sum()/lmd.sum()
print(f'第{k}主成分までの累積寄与率 = {ck:.3f}')

第2主成分までの累積寄与率 = 0.917

推薦

15 次元削減後の評価履歴

np.hstack()を使ってx3ruを横に結合する。np.hstack()の引数に()を入れないといけないことや、rureshapeを使う必要があることに注意。
python
Du2 = np.hstack((x3, ru.reshape(-1, 1)))
print(f'Du\' = \n{Du2}')

Du' = [[ 0.699 0.264 1. ] [-0.139 1.065 1. ] [ 0.752 1.355 1. ] [-2.564 0.202 -1. ] [ 1.969 -0.636 -1. ] [ 0.373 -1.211 -1. ] [-2.035 -0.496 -1. ] [ 1.219 0.656 nan] [-0.545 1.766 nan] [-1.788 -0.601 nan] [ 1.598 -0.535 nan] [-0.148 -1.603 nan] [ 0.611 -0.227 nan]]

第8章 評価値行列の次元削減

準備
python
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))

分散共分散行列

01 各アイテムに対して与えられた平均中心化評価値の平均値

R2で行方向に平均をとればよい。np.nanmean()によってnp.nanの処理を行う必要があることも忘れずに。
python
ri2_mean = np.nanmean(R2,axis = 0)
print('ri2_mean = {}'.format(ri2_mean))

ri2_mean = [ 0.367 0.588 0.4 -0.037 -0.938 -0.383]

02 各アイテムの平均中心化評価値の分散

01と同じようにして分散をとればよい。
python
s2 = np.nanvar(R2,axis = 0)
print(f's^2 = {s2}')

s^2 = [0.336 1.348 0.505 1.279 0.137 0.494]

03 アイテムiiとアイテムjjの平均中心化評価値の共分散

共分散をとる際に、np.nanが絡むので今回はnp.covではなくnp.dotで内積をとる形で値を求める。R2[Uij, i]i列目かつi列目とj列目でnp.nanでない行のみを取得することができるのだが、数式の定義にもあるように平均との差分をとる部分とUijの要素数で割る部分を忘れずに実装する。
python
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}')

s01 = 0.892

04 分散共分散行列

03で行った処理を自分で関数化してから2重リスト内包表記を使って解いた。関数my_sijは途中のUijのサイズが0の場合Sの値を0に変更する必要があるためUij.sizeも返り値にふくめているが結局関数の中でゼロ割が起こるのでそこで発生したnp.nan0にしてしまうやり方のほうがよさそうだなと思った。
python
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
python
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}')

S = [[ 0.336 0.892 0.169 -0.659 -0.057 -0.572] [ 0.892 1.348 0.505 -1.466 0.166 -0.817] [ 0.169 0.505 0.505 -0.655 -0.183 -0.27 ] [-0.659 -1.466 -0.655 1.279 -0.109 0.752] [-0.057 0.166 -0.183 -0.109 0.137 -0.015] [-0.572 -0.817 -0.27 0.752 -0.015 0.494]]

固有値・固有ベクトル

05 固有値・固有ベクトル

LA.eig()で計算すればよい。vの値の符号が逆になってしまっているのだが、これは問題ないはず。
以下のサイトで、符号が逆でも問題ないらしいことを確認している。主成分得点に関するサイトで、下のほうに記載がある。
python
lmd, v = LA.eig(S)
print(f'λ = {lmd}')
print(f'v = \n{v}')

λ = [ 3.909 0.48 0.233 -0.315 -0.049 -0.16 ] v = [[-0.327 -0.228 -0.484 0.685 0.279 -0.245] [-0.609 -0.211 0.099 -0.565 0.371 -0.344] [-0.245 0.806 0.097 0.134 -0.202 -0.472] [ 0.583 -0.126 -0.374 -0.258 -0.019 -0.661] [-0.028 -0.462 0.624 0.294 -0.394 -0.393] [ 0.348 0.157 0.465 0.204 0.767 -0.087]]

06 第dd主成分までの固有ベクトル

vについて固有値の大きさ順に列を並び替えた上で、DIM列までをスライシングすればよい。
python
indices = np.argsort(-lmd)
V = v[:, indices][:, :DIM]
print(f'V = \n{V}')

V = [[-0.327 -0.228] [-0.609 -0.211] [-0.245 0.806] [ 0.583 -0.126] [-0.028 -0.462] [ 0.348 0.157]]

主成分得点

07 ユーザuuの第kk主成分得点

第7章の11でもやったように内積を計算すればよいのだが、直接R2[u, :]V[:, k]を計算するとnp.nanのせいでよくないことに気をつけなければならない。
python
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}')

p00 = -0.474

08 潜在因子行列

第7章の12でもやったように行列計算をすればよいのだが、前問のように直接R2Vを計算するとnp.nanのせいでよくないのでnp.nanを排除して2重リスト内包表記で求めることになる。今回は関数my_pukを用意してnp.nanを排除した上で計算を行う。
python
def my_puk(u, k):
    puk = np.dot(R2[u, Iu[u]], V[Iu[u], k]) / Iu[u].size
    return puk
python
P = np.array([[my_puk(u, k) for k in range(DIM)] for u in U])
print(f'P = \n{P}')

P = [[-0.474 0.127] [-0.251 -0.027] [-0.195 0.463] [-0.214 -0.017] [ 0.445 -0.009]]

第9章 単純ベイズ分類器

準備
python
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]

問題設定

省略

事前確率

01 評価値がrrとなる事前確率(分子)

問題01から09では、一部コードのみ与えられた関数P_priorP_condPを完成させていく。
01では、事前確率の分子を求める。説明から、与えられたrrと同じ評価値を持つアイテムの数を数えればよい。
python
# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
np.where(ru == r, 1, 0).sum()

6

02 評価値がrrとなる事前確率(分母)

02では、事前確率の分母を求める。説明から、評価値を持つアイテムの数を数えればよい。
python
# 以下、試しに計算するのに用いたコードとその計算結果
ruL.size

10
python
# 問題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
python
r = +1
print(f'P(R={r:+}) = {P_prior(r)}')
r = -1
print(f'P(R={r:+}) = {P_prior(r)}')

P(R=+1) = 6/10 P(R=-1) = 4/10

特徴量kkに関する条件付き確率

03 特徴量kkに関する条件付き確率(分子)

03では、与えられた評価値rrと評価値が一致し、なおかつ特徴量kkに関する部分が同じアイテムの数を数えることで望みの結果を得ることができる。
python
# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
k = 1
i = 2
Du[:, k][(Du[i, k] == Du[:, k])&(ru == r)].size

3

04 特徴量kに関する条件付き確率(分母)

04では、与えられたrrに評価値が一致するアイテムの数iiを数えることで望みの結果を得ることができる。
python
# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
ru[ru == r].size

6
python
# 問題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
python
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)))

P(X0=x10,0|R=+1) = 4/6 P(X0=x10,0|R=-1) = 0/4

嗜好予測

05 好き嫌いの確率

事前確率ppと、各特徴量に関する条件付き確率の積を書けることで計算すればよい。warningが発生しているが、解決は時間があればまた後でおこなうつもり。
python
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
python
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}')

P(R=+1|1,1,0,1,1,0) = 6/10×4/6×3/6×6/6×2/6×4/6×4/6 = 0.030 P(R=-1|1,1,0,1,1,0) = 4/10×0/4×1/4×1/4×1/4×3/4×2/4 = 0.000
/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:23: DeprecationWarning: Fraction.__float__ returned non-float (type numpy.float64). The ability to return an instance of a strict subclass of float is deprecated, and may be removed in a future version of Python.

ラプラススムージング

06 評価値がrrとなる事前確率(分子)(ラプラススムージングあり)

ほぼ問題01と同じように解く。ALPHAだけは足すようにする。
python
# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
np.where(ru == r, 1, 0).sum() + ALPHA

7

07 評価値がrrとなる事前確率(分母)(ラプラススムージングあり)

ほぼ問題02と同じように解く。N*ALPHAだけは足すようにする。
python
# 以下、試しに計算するのに用いたコードとその計算結果
ruL.size + N * ALPHA

12
python
# 問題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

08 特徴量kkに関する条件付き確率(分子)(ラプラススムージングあり)

ほぼ問題03と同じように解く。ALPHAだけは足すようにする。
python
# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
k = 1
i = 2
Du[:, k][(Du[i, k] == Du[:, k])&(ru == r)].size + ALPHA

4

09 特徴量kkに関する条件付き確率(分母)(ラプラススムージングあり)

ほぼ問題04と同じように解く。ALPHAMの第kk成分の積だけは忘れず足すようにする。
python
# 以下、試しに計算するのに用いたコードとその計算結果
r = 1
k = 1
ru[ru == r].size + ALPHA * M[k]

8
python
# 問題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

推薦

10 ユーザuuのアイテムiiに対するスコア

問題10では、一部コードのみ与えられた関数scoreを完成させる。やることとしては問題05でみたP(i, r)を使って、P(i, +1)[2] / (P(i, +1)[2] + P(i, -1)[2])を計算してあげればよい。
python
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
python
u = 0
scores = {i: score(u, i) for i in Iu_not}
print('scores = ')
pprint.pprint(scores)

scores = {10: 0.9646054787625311, 11: 0.055176912846500135, 12: 0.18936236007174226}
/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:23: DeprecationWarning: Fraction.__float__ returned non-float (type numpy.float64). The ability to return an instance of a strict subclass of float is deprecated, and may be removed in a future version of Python.

11 推薦リスト

scoresの作り方をいじって、if文によりTHETAよりもscore(u, i)が小さいものを除去してやる必要がある。
python
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
python
u = 0
rec_list = order(u, Iu_not)
print('rec_list = ')
for i, scr in rec_list:
    print('{}: {:.3f}'.format(i, scr))

rec_list = 10: 0.965
/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:23: DeprecationWarning: Fraction.__float__ returned non-float (type numpy.float64). The ability to return an instance of a strict subclass of float is deprecated, and may be removed in a future version of Python.

第10章 決定木

準備
python
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]

ジニ係数

01 「好き」な事例が含まれる割合

問題01から03では、一部コードのみ与えられた関数Gを完成させる。問題01では、評価値を与えられたアイテムの数で「好き」と評価されたアイテムの数を割ってあげてppを計算する。

02 「嫌い」な事例が含まれる割合

問題01と同じように計算すればよい。

03 ジニ係数

定義通りに計算すればよい。
python
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
python
print(f'G(DuL) = {G(DuL):.3f}')

G(DuL) = 0.480

分割の良さ

04 特徴量kkを含まない訓練事例集合

問題04から06では、一部コードのみ与えられた関数G_partitionedとその結果を表示するためのコードを完成させる。問題04では、DuLから訓練データから特徴量kk0となる行のみを抜き出すような処理を書く。特徴量kkの列に注目して、インデクシングを行えばよい。

05 特徴量kkを含む訓練事例集合

問題05では、DuLから訓練データから特徴量kk1となる行のみを抜き出すような処理を書く。特徴量kkの列に注目して、インデクシングを行えばよい。

06 特徴量kkを基準に分割したときのジニ係数

問題06では、特徴量kkを基準に分割したときのジニ係数を求める。定義式は与えられているので、その数式通りに計算してあげればよい。
python
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
python
# 特徴量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}')

DuL0 = [[ 0. 1. 0. 0. 1. 0. 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.]] DuL1 = [[1. 0. 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.]] G(DuL → [DuL0, DuL1]) = 0.267

決定木の学習

07 レベル0の選択基準

与えられた関数get_ginisを使ってジニ係数を最小とするような特徴量を考える。辞書であるginisから、バリューが最小値になるようなキーを求める。最小値をとるバリューに対応するキーを求める方法としてmin()の引数keydictname.getを用いる方法があることを以下のサイトで知ったのでそれを使う。
python
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
python
# レベル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}')

ginis = {0: 0.26666666666666666, 1: 0.45, 2: 0.17142857142857146, 3: 0.4761904761904763, 4: 0.4761904761904763, 5: 0.4666666666666666} k0 = 2 DuL0 = [[ 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. 1. 0. 0. 1. 1. -1.]] DuL1 = [[ 0. 0. 1. 0. 1. 0. -1.] [ 0. 0. 1. 1. 1. 0. -1.] [ 0. 0. 1. 0. 0. 1. -1.]]

08 レベル1の選択基準

07と同じことをDuLではなくDuL0に対して行う。
python
# レベル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}')

k1a = 0 DuL00 = [[ 0. 1. 0. 0. 1. 0. 1.] [ 0. 1. 0. 1. 0. 1. 1.] [ 0. 1. 0. 0. 1. 1. -1.]] DuL01 = [[1. 0. 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.]]

09 レベル2の選択基準

07、08と同じように計算すればよい。
python
# レベル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}')

k2a = 3 DuL000 = [[ 0. 1. 0. 0. 1. 0. 1.] [ 0. 1. 0. 0. 1. 1. -1.]] DuL001 = [[0. 1. 0. 1. 0. 1. 1.]]

嗜好予測

10 予測対象データに対する嗜好予測

問題10では、与えられた関数trainpredictを使って予測結果を出力する処理を実行する。
python
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)
python
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))

dtree = {0: 2, 1: 0, 3: 3, 7: 5, 15: '+1', 16: '-1', 8: '+1', 4: '+1', 2: '-1'} ruU_pred = {10: 1, 11: -1, 12: -1}

第11章 嗜好予測の正確性

準備
python
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],
])

平均絶対誤差

01 推薦システムAのMAE

問題01では、RARを比較したMAEのMAE_Aを計算する。分母は非np.nanであるデータの個数として、分子はR - RAの絶対値をnp.abs(R - RA)でとってあげて親の顔ほどはみたnp.nanを無視した和をとることで計算できる。

02 推薦システムBのMAE

問題01と同様の方法で計算可能である。
python
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}')

MAE_A = 0.800 MAE_B = 1.000

平均二乗誤差

03 推薦システムAのMSE

問題03では、RARを比較したMSEのMSE_Aを計算する。分母は非np.nanであるデータの個数として、分子はR - RAの2乗誤差を(R - RA)**2でとってあげて親の顔ほどはみたnp.nanを無視した和をとることで計算できる。

04 推薦システムBのMSE

03とほとんど同じように計算できる。
python
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}')

MSE_A = 1.600 MSE_B = 1.000

二乗平均平方根誤差

05 推薦システムAのRMSE

MSE_Aの平方根をとればよい。

06 推薦システムBのRMSE

05と同じ。
python
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}')

RMSE_A = 1.265 RMSE_B = 1.000

正規化MAEと正規化RMSE

07 推薦システムAのNMAE

08 推薦システムBのNMAE

09 推薦システムAのNRMSE

10 推薦システムBのNRMSE

R_MAXR_MINnp.nanmin()np.nanmax()で求めて、その差で割ればよい。
python
# 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}')

NMAE_A = 0.267 NMAE_B = 0.333 NRMSE_A = 0.422 NRMSE_B = 0.333

第12章 好き嫌い分類に基づく評価指標 テストデータと推薦リスト

準備
python
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],
])

混同行列

01 好きなアイテムか否かの判定

問題01から06では一部コードのみ与えられた関数confusion_matrixを完成させる。
01では、RSに対して4以上の値をとっていればTrue、それ以外の場合はFalseとなるような変数likeを作る。今回はRSnp.nanでないところのみを抽出したあとでインデキシングを使うことで作成した。

02 推薦されたアイテムか否かの判定

np.argsort(-ndarray)を使って、その結果がK未満の成分のインデックスを取り出せばよい。

03 好きなアイテムが推薦された数(TP)

np.logical_and(like, recommended)を利用してlikerecommendedTrueであればTrueを返すようなndarrayを作ればよい。これに対してTrueの数を数えるには、np.count_nonzero()を利用する。

04 好きなアイテムが推薦されなかった数(FN)

03とほぼ同じ操作をすればよい。

05 嫌いなアイテムが推薦された数(FP)

03とほぼ同じ操作をすればよい。

06 嫌いなアイテムが推薦されなかった数(TN)

03とほぼ同じ操作をすればよい。
python
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
python
u = 0
K = 3
TP, FN, FP, TN = confusion_matrix(u, RA, K)
print(f'混同行列 = \n{np.array([[TP, FN], [FP, TN]])}')

like = [ True True False True True False False] recommended@3 = [ True False True False True False False] TP@3 = 2 FN@3 = 2 FP@3 = 1 TN@3 = 2 混同行列 = [[2 2] [1 2]]

真陽性率と偽陽性率

07 真陽性率(TPR)

TPRの定義通りに計算すればよい。

08 偽陽性率(FPR)

FPRの定義通りに計算すればよい。
python
TPR = TP / (TP + FN)
print(f'TPR@{K} = {TPR:.3f}')
FPR = FP / (FP + TN)
print(f'FPR@{K} = {FPR:.3f}')

TPR@3 = 0.500 FPR@3 = 0.333

適合率と再現率

09 適合率

10 再現率

11 F値

すべて定義通りに計算すればよい。
python
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}')

precision@3 = 0.667 recall@3 = 0.500 F1@3 = 0.571

第13章 推薦順位に基づく正確性 テストデータと推薦リスト

準備
python
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

平均逆順位

01 好きなアイテムか否かの判定

Rの各要素が4以上ならばTrueを返すような比較を行う。今回は単純に>=による比較を行った。
python
np.nanmin(RA[0, R[0] >= 4])

1.0

02 最初に好きなアイテムが見つかったときの順位

各ユーザーのlikeTrueであるアイテムの中で、対応するRAの値が最も小さいものをとってくればよい。そのため、RAu行目かつその成分に対応するlikeの行がTrueになっているものをインデキシングで参照できるようにする。また、np.nanがあるので最小値を得るためにはnp.nanmin()を用いる。

03 MRR

kuの各成分の逆数をとったリストの和を分子、ユーザーの数を分母とした値をMRRに格納すればよい。
python
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}')

like = [[ True True False False True True False False False False] [False False False False False False True False True False] [ True False False True True False False False False False]] ku = [1. 2. 3.] MRR = 0.611

平均適合率

04 評価値行列の並べ替え

ユーザuの評価情報であるRu行目について、RAの昇順に並べ替えるためのインデックスをnp.argsort()によって取得する。

05 好きなアイテムか否かの判定

04で作ったranked_Rに対して、>=による比較の結果を得ればよい。

06 好きなアイテムか否かの判定

05で作ったranked_likeに対して、np.where(ranked_like, 1, 0)とすれば望みの結果を得ることができる。

07 各ユーザのAP

リスト内包表記でAPuを作成する。そのために、06で作ったrelu行目に対してインデックスTOP_Kまでスライシングしたものと、precisionsu行目を同じくインデックスTOP_Kまでスライシングしたものを用意する。これらの用意ができたら、APuAP_uの定義にある計算を行えばよい。

08 MAP

MAPMAPの定義をそのまま計算する。
python
# 各順位における適合率
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}')

precisions = [[1.0, 1.0, 0.6666666666666666, 0.75, 0.6, 0.6, 0.6], [0.0, 0.5, 0.3333333333333333, 0.25, 0.4, 0.4, 0.4], [0.0, 0.0, 0.3333333333333333, 0.5, 0.4, 0.4]] ranked_R = [[ 5. 4. 3. 5. 2. 4. nan 2. nan nan] [ 3. 5. 3. 3. 4. 3. 2. nan nan nan] [ 3. 3. 5. 4. 3. 4. nan nan nan nan]] ranked_like = [[ True True False True False True False False False False] [False True False False True False False False False False] [False False True True False True False False False False]] rel = [[1 1 0 1 0 1 0 0 0 0] [0 1 0 0 1 0 0 0 0 0] [0 0 1 1 0 1 0 0 0 0]] APu = [0.917 0.45 0.417] MAP = 0.594

DCG

09 各ユーザのDCG

定義通り計算していく。列を指定するためのインデックスにはIではなくIu_recを使うこと。

10 理想的な推薦順位

argsort()を2重に使うことでRIが計算可能である。注意点は、降順なので符号を逆にして引数に指定する必要があること。正直なところ仕組みがよくわからなかったので、以下のサイトを参照して方法だけわかったという感じ。

11 理想的な推薦リスト

RIu行目についてTOP_K以下であるような成分のインデックスをとってIにインデキシングをすれば望みの結果であるIu_recI[u]を得ることができる。

12 各ユーザのIDCG

DCGuを計算したときの処理とほとんど同じ処理をすればよい。ただし、分母で用いる順位k_iに相当するものは、理想的な順位であるRIからとってくる必要がある。

13 各ユーザのnDCG

定義に沿って、単純な割り算を行えばよいはず。

14 nDCG

nDCGuの平均をとればよい。
python
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))

DCGu = [14.254 13.115 12.447] RI = [[ 1 3 5 8 2 4 6 7 9 10] [ 3 4 5 6 7 8 2 9 1 10] [ 2 7 4 1 3 5 8 6 9 10]] Iu_recI = [[0 1 2 4 5] [0 1 2 6 8] [0 2 3 4 5]] IDCGu = [15.816 13.685 14.316] nDCGu = [0.901 0.958 0.869] nDCG = 0.910
長く拙い解答でしたが、いかがだったでしょうか。recsys-pythonを作ってくださった方、これを見てくださった方本当にありがとうございました。

Discussion

コメントにはログインが必要です。