【R言語】友達に漫画をおススメするときに利用するレコメンドアルゴリズムについて

 岩波データサイエンスVol.5に行列分解を利用したレコメンドについて記事があったので、雰囲気だけでも理解しようと簡単な例を作成してみました。売れ筋の漫画10タイトルの購入の有無を7人について調査し、ある人(小林さん)がまだ購入してない漫画のうち、どの漫画をレコメンドしたらいいのかについてです。売れ筋漫画についてはジュンク堂書店ランキング(漫画・コミックのジュンク堂書店書店ランキング - hontoベストセラー)の上位10タイトルの漫画(2017年6月18日現在)を利用しました。今回取り上げるレコメンドアルゴリズム協調フィルタリング(ユーザベースド、アイテムベースド)、特異値分解(SVD)、非負値行列因子分解(NMF)についてです。

目次


ちなみにブログの作成にあたっては、以下の記事を参考にさせていただきました。

tech-blog.fancs.com

  • 特異値分解(SVD)、非負値行列因子分解(NMF)について

smrmkt.hatenablog.jp

データセットの作成

> res <- c(1,0,0,0,0,0,0,0,1,0,
>          0,1,1,0,0,0,1,0,0,1,
>          1,1,1,0,0,0,0,0,1,1,
>          0,0,1,0,1,0,1,0,0,0,
>          1,1,0,1,0,1,0,1,0,0,
>          1,1,0,0,0,0,0,0,0,1,
>          0,1,1,0,1,0,0,0,0,1)
> 
> item <- c("僕のヒーローアカデミア","魔法科高校の劣等生","図書館戦争",
>                      "中間管理録トネガワ","orange","ポプテピピック2nd","黒執事",
>                      "1日外出録ハンチョウ","ドラゴンボール超","幼女戦記")
> 
> user <- c("佐藤","鈴木","橋本","菅野","松本","坂本","小林")
> 
> mat <- t(matrix(res, 10,7))
> colnames(mat) <- item
> rownames(mat) <- user
> (mat)

	僕のヒーローアカデミア	魔法科高校の劣等生	図書館戦争	中間管理録トネガワ	orange	ポプテピピック2nd	黒執事	1日外出録ハンチョウ	ドラゴンボール超	幼女戦記
佐藤	1	0	0	0	0	0	0	0	1	0
鈴木	0	1	1	0	0	0	1	0	0	1
橋本	1	1	1	0	0	0	0	0	1	1
菅野	0	0	1	0	1	0	1	0	0	0
松本	1	1	0	1	0	1	0	1	0	0
坂本	1	1	0	0	0	0	0	0	0	1
小林	0	1	1	0	1	0	0	0	0	1

f:id:joure:20170619205729p:plain

使用するパッケージ

> library(NMF)
> library(dplyr)

協調フィルタリング

類似度を計算する関数

 Rで学ぶ推薦システム[part1 類似度](Rで学ぶ推薦システム - F@N Ad-Tech Blog)の記事と同様に3つの尺度(ユークリッド距離、コサイン類似度、ピアソンの積率相関係数)を計算してみます。具体的には以下のとおりです。

> get_dist <- function (v1, v2, method){
>     if(method =="euclidean"){
>         return(1/sqrt(sum((v1 - v2)^2)))
>     } else if(method == "cosine"){
>         return(v1 %*% v2 / (sqrt(sum(v1^2))*sqrt(sum(v2^2))))
>     } else { # pearson
>         return(cov(v1,v2) / (sd(v1) * sd(v2)))
>     }
> }
ユーザベースド

 ここでのユーザベースドにおける協調フィルタリングの流れは次のとおりです。まずユーザをレコメンド対象の小林さんとその他に分けて、小林さんとの類似度を計算をします。次に類似度が高い上位3人をピックアップし、その3人を利用して小林さんが購入していな漫画についてレコメンドします。今回は単純な平均値を使用しますが、類似度のウェイト付き平均値を利用することもあるそうです。

類似度の算出
> ufam <- cbind(
> apply(mat[1:6,],MARGIN = 1, FUN= function(x) get_dist(x,mat["小林",],"euclidean")),
> apply(mat[1:6,],MARGIN = 1, FUN= function(x) get_dist(x,mat["小林",],"cosine")),
> apply(mat[1:6,],MARGIN = 1, FUN= function(x) get_dist(x,mat["小林",],"pearson"))
> )
> colnames(ufam) <- c("euclidean","cosine","pearson")
> print(round(ufam,digit=2))
     euclidean cosine pearson
佐藤      0.41   0.00   -0.41
鈴木      0.71   0.75    0.58
橋本      0.58   0.67    0.41
菅野      0.58   0.58    0.36
松本      0.38   0.22   -0.41
坂本      0.58   0.58    0.36

今回はコサイン類似度を利用して、小林さんとの類似度が高い3人をピックアップします。

> top3 <- head(sort(ufam[,"cosine"], decreasing = TRUE),3)
> print(round(nn3,digit=2))

鈴木 橋本 菅野 
0.75 0.67 0.58 
小林さんが購入していない漫画をレコメンド

 データセットの行列から行について小林さんとの類似度が高い3人をピックアップし、列については小林さんが購入していない漫画をピックアップした行列を以下のように作成します。

> top3_names <- names(top3)
> notbuy <- mat["小林",][mat["小林",]==0]
> notbuy_names <- names(notbuy)
> reco <- mat[top3_names, notbuy_names]
> reco

	僕のヒーローアカデミア	中間管理録トネガワ	ポプテピピック2nd	黒執事	1日外出録ハンチョウ	ドラゴンボール超
鈴木	0	0	0	1	0	0
橋本	1	0	0	0	0	1
菅野	0	0	0	1	0	0

 上記の表の各カラムの平均値を算出し、その値が高い漫画が小林さんにおススメの漫画ということになります。具体的には以下の漫画となります。

> sort(colMeans(reco),decreasing = TRUE)

黒執事
0.67
僕のヒーローアカデミア
0.33
ドラゴンボール超
0.33
中間管理録トネガワ
0
ポプテピピック2nd
0
1日外出録ハンチョウ
0

アイテムベースド

 アイテムベースドにおける協調フィルタリングの流れは次のとおりです。データセットの各列について、小林さんが購入した漫画と購入していない漫画で分割します。次に、小林さんが購入した漫画と購入していない漫画との類似度を計算し、購入していない漫画についての類似度の合計を評価値としてレコメンドします。

データの分割
# 小林さんが購入した漫画
> buy <- mat["小林",][mat["小林",]==1]
> buy_names <- names(b)
> buy_kbys <- mat[,buy_names]
> buy_kbys

	魔法科高校の劣等生	図書館戦争	orange	幼女戦記
佐藤	0	                0	        0	0
鈴木	1	                1	        0	1
橋本	1	                1	        0	1
菅野	0	                1	        1	0
松本	1	                0	        0	0
坂本	1	                0	        0	1
小林	1	                1	        1	1


# 小林さんが購入していない漫画
> notbuy_kbys <-mat[,notbuy_names]
> notbuy_kbys

	僕のヒーローアカデミア	中間管理録トネガワ	ポプテピピック2nd	黒執事	1日外出録ハンチョウ	ドラゴンボール超
佐藤	1	                0	                 0	                0	 0	                 1
鈴木	0	                0	                 0	                1	 0	                 0
橋本	1	                0	                 0	                0	 0	                 1
菅野	0	                0	                 0	                1	 0	                 0
松本	1	                1	                 1	                0	 1	                 0
坂本	1	                0	                 0	                0	 0	                 0
小林	0	                0	                 0	                0	 0	                 0
類似度の算出

 小林さんが購入した漫画と購入していない漫画との類似度の計算結果は以下のとおりです。

> ifam <- cbind(
>     apply(notbuy_kbys,MARGIN = 2, FUN= function(x) get_dist(x,buy_kbys[,"魔法科高校の劣等生"],"cosine")),
>     apply(notbuy_kbys,MARGIN = 2, FUN= function(x) get_dist(x,buy_kbys[,"図書館戦争"],"cosine")),
>     apply(notbuy_kbys,MARGIN = 2, FUN= function(x) get_dist(x,buy_kbys[,"orange"],"cosine")),
>     apply(notbuy_kbys,MARGIN = 2, FUN= function(x) get_dist(x,buy_kbys[,"幼女戦記"],"cosine"))
> )
> colnames(ifam) <-colnames(buy_kbys)
> (round(t(ifam),digit=2))

                   僕のヒーローアカデミア 中間管理録トネガワ ポプテピピック2nd
魔法科高校の劣等生                   0.67               0.45              0.45
図書館戦争                           0.25               0.00              0.00
orange                               0.00               0.00              0.00
幼女戦記                             0.50               0.00              0.00
                   黒執事 1日外出録ハンチョウ ドラゴンボール超
魔法科高校の劣等生   0.32                 0.45             0.32
図書館戦争           0.71                 0.00             0.35
orange               0.50                 0.00             0.00
幼女戦記             0.35                 0.00             0.35
小林さんが購入していない漫画をレコメンド

 上のifamの転置行列の結果から、小林さんが購入していない漫画(各列)の合計値を評価値とし、その値が高い漫画が小林さんにおススメする漫画となります。具体的には以下のとおりです。

> round(sort(rowSums(ifam), decreasing=T),2) # ifam行列は転置していないので行の合計とする

黒執事
1.88
僕のヒーローアカデミア
1.42
ドラゴンボール超
1.02
中間管理録トネガワ
0.45
ポプテピピック2nd
0.45
1日外出録ハンチョウ
0.45

特異値分解(SVD)

 岩波データサイエンスVol.5に平易かつ分かりやすい記事がありますので説明はそちらを見ていただければとし、以下にコードを示します。

> res.svd <- svd(mat)
> u <- res.svd$u
> v <- res.svd$v
> d <- diag(res.svd$d)
> d_r <- d
> for (i in 3:7) {
>     d_r[i,i] = 0
> }
> d.inv <- diag(1/res.svd$d)
> x.svd <- mat %*% v %*% d.inv %*% d_r %*% t(v)
> colnames(x.svd) <- colnames(mat)
> (round(x.svd,digit=2))

	僕のヒーローアカデミア	魔法科高校の劣等生	図書館戦争	中間管理録トネガワ	orange	ポプテピピック2nd	黒執事	1日外出録ハンチョウ	ドラゴンボール超	幼女戦記
佐藤	0.60	0.41	-0.07	0.24	-0.15	0.24	-0.15	0.24	0.23	0.17
鈴木	0.19	0.84	1.07	-0.07	0.56	-0.07	0.56	-0.07	0.16	0.91
橋本	0.91	1.20	0.77	0.25	0.26	0.25	0.26	0.25	0.43	0.95
菅野	-0.32	0.30	0.83	-0.23	0.52	-0.23	0.52	-0.23	-0.06	0.53
松本	1.24	0.87	-0.12	0.49	-0.30	0.49	-0.30	0.49	0.49	0.38
坂本	0.82	0.88	0.39	0.26	0.06	0.26	0.06	0.26	0.36	0.61
小林	0.19	0.84	1.07	-0.07	0.56	-0.07	0.56	-0.07	0.16	0.91
> round(sort(x.svd["小林",],decreasing=T),digit=2)

図書館戦争
1.07
幼女戦記
0.91
魔法科高校の劣等生
0.84
orange
0.56
黒執事
0.56
僕のヒーローアカデミア
0.19
ドラゴンボール超
0.16
中間管理録トネガワ
-0.07
ポプテピピック2nd
-0.07
1日外出録ハンチョウ
-0.07

小林さんが購入した漫画を除くと『黒執事』が最も評価値が高いことが分かります。これは協調フィルタリングでのレコメンド結果と一致しています。

非負値行列因子分解(NMF)

 特異値分解(SVD)の分析結果をみると行列の要素に負の値があり解釈がしづらいところです。NMFでは非負値性の制約条件を追加して分解をしているため、行列の要素が非負となっています。詳細については岩波データサイエンスVol.5に記載されていますので、ここではコードのみ示します。

> res.nmf <- nmf(mat, 2, seed=1234)
> w <- basis(res.nmf)
> h <- coef(res.nmf)
> x.nmf <- w %*% h
> round(x.nmf,digit=2)

	僕のヒーローアカデミア	魔法科高校の劣等生	図書館戦争	中間管理録トネガワ	orange	ポプテピピック2nd	黒執事	1日外出録ハンチョウ	ドラゴンボール超	幼女戦記
佐藤	0.74	0.35	0.00	0.18	0.00	0.18	0.00	0.18	0.37	0.00
鈴木	0.00	0.83	1.06	0.00	0.53	0.00	0.53	0.00	0.00	1.06
橋本	0.90	0.95	0.68	0.22	0.34	0.22	0.34	0.22	0.45	0.68
菅野	0.00	0.62	0.79	0.00	0.40	0.00	0.40	0.00	0.00	0.79
松本	1.84	0.86	0.00	0.46	0.00	0.46	0.00	0.46	0.92	0.00
坂本	0.53	0.57	0.41	0.13	0.21	0.13	0.21	0.13	0.26	0.41
小林	0.00	0.83	1.06	0.00	0.53	0.00	0.53	0.00	0.00	1.06
> round(sort(x.nmf["小林",],decreasing=T),digit=2)

図書館戦争
1.06
幼女戦記
1.06
魔法科高校の劣等生
0.83
orange
0.53
黒執事
0.53
僕のヒーローアカデミア
0
ドラゴンボール超
0
中間管理録トネガワ
0
ポプテピピック2nd
0
1日外出録ハンチョウ
0

NMFの結果も『黒執事』が小林さんにおススメする漫画ということになりました。

おわりに

  • すべてのレコメンドアルゴリズムにおいて小林さんにおススメする一押しの漫画は『黒執事』となりました。
  • 特異値分解(SVD)は負の値を有する要素が多く解釈しにくい一方、非負値行列因子分解(NMF)はすべての要素が正の値となっている。
  • 他にもFactorization Machines (FM)というレコメンド手法があるらしい。