畳庵〜tatamiya practice〜

機械学習・統計モデリング練習帳。読書・source code読みの記録や,実装など。

レコメンドの「模様」を描く〜Matrix Factorizationの因子数比較〜

※2020/03/08

本稿の記述には誤りがあります。

今回適用したアルゴリズムでは,正則化項は入れておらず,代わりにUser, Itemごとの平均的な評価を表すBias項が入っていました。

本日中に訂正します。

2020/03/08 13:20 修正しました。


にわかレコメンド屋さん,始めました。

協調性がないので,まずは協調フィルタリングから勉強しています。

今回のお題

さて,協調フィルタリングというと,最初にベクトル類似度に基づく近傍法,その次にMatrix Factorization(因子分解法ともいう)がくると思います。

こうしたレコメンドアルゴリズムでは,User-Item行列を元に各Userの各Itemに対する評価を推定して新たなレコメンド評価値を作成します。

このとき,入力の評価値とモデルの出力するレコメンド評価値は,どのように対応しているのでしょうか?

これを視覚的に捉えるために,User-Item行列と出力レコメンド評価値をヒートマップにして可視化してみました。

特に今回は,Matrix Factorizationを中心に扱い,Itemカテゴリ数に対応する重要なハイパーパラメータ集約次元数( dと表す)を変えた時の振る舞いについて見ていきたいと思います。

計算・描画に用いたPythonコードは以下にアップしましたので,併せてご覧ください。

https://gist.github.com/tatamiya/439728c3e0b61bb1d0e9f59194031729

アルゴリズムの概要

Matrix Factorizationの一般的な話

ここではMatrix Factorizationについて簡単に説明します。

User-Item行列 Mは全Userの,各Itemに対する評価を行列に表したものです。

したがって,User数を n_u, Item数を m_iとすると, M n_u \times m_i次元となります。

この行列を以下のように分解近似するのが,Matrix Factorizationの基本的な考え方になります:

 M \simeq  P_d Q_d

ここで, P_dは大きさ n_u \times d Q_d d \times m_iの行列で,整数 dはハイパーパラメータです。

 dは因子数で,上記の分解がうまく行った場合はItemカテゴリ数に相応すると考えられています。

つまり,行列 P_dは各Userの各カテゴリに対する評価・嗜好性, Q_dは各Itemのカテゴリ成分(映画なら「コメディ: 50%, ドキュメンタリ: 10%,アニメ: 40%」など) のように解釈が可能になります。

Funkの方法

今回扱うアルゴリズムは,レコメンドにおけるMatrix Factorizationの中でも,Netflix PrizeにてSimon Funkが用いて一般的に広まるきっかけとなった手法*1を元にしています。

上記で説明したものとの一番の違いは,Bias項といって,Userごと,Itemごとの平均値に相当するものを入れている点です。

これにより,あるUser  uの各Item  iに対するレコメンド評価値 \hat{r}_{u,i}は,以下のように表されます:

 \hat{r}_{u,i}=\mu + b_{u}+b_{i} + \sum_k^{d} p_{u, k} q_{k, i}

 \mu, b_{u}, b_{i}は,それぞれ,

  •  \mu: 全体の平均評価値
  •  b_{u}: User  uの平均的な評価傾向
  •  b_{I}: Item  iへの,全Userの平均的な評価傾向

を表しています。

後半の \sum_k^{d} p_{u, k} q_{k, i}は,分解後の行列の積です。

前述の因子数とカテゴリ数の関係をもとに考えると,この項はUserのカテゴリ嗜好性に基づいて算出された補正項と捉えられそうです。

以上から,レコメンド評価値は,

  • バイアス項: User, Itemの平均的な評価値をもとに算出した予測値
  • 行列分解項: ユーザーのカテゴリー嗜好性に基づく補正項

の2つから成ると考えることにしましょう。

そのほかにも,分解後の行列成分が大きくなりすぎないよう,正則化項を入れる場合もありますが,今回は割愛します。

ヒートマップによる可視化

可視化の前に,まず使用するデータについてです。

サンプルデータとしては,定番のMovieLensという,映画を0.5~5で数値評価したデータを用いたいと思います。*2

User数は610人,Item数は評価があるもので9,724個なので,User-Item行列は 610 \times 9,724の大きさです。

全部で約600万成分ありますが,実際に評価が行われているのは100,836件なので,User-Item行列はほとんどの成分が空白な疎行列となっています。

これをもとに,Matrix Factorizationでモデルを作成し,全User, Itemのレコメンド評価値を算出したいと思います。

入力データ

まずはUser-Item行列を描いてみます。

f:id:tatamiyatamiyatatatamiya:20200307190517p:plain

色が濃いところほど評価が低く,明るいオレンジほど高評価になります。

白い部分はUserの評価がない部分です。

最適な因子数での比較

まず最初に,GridSearchで最適パラメータを探したところ, d=16の時RMSEが最小になりました。

この時のレコメンド評価値をMatrixで表し,描画したのが以下です。

f:id:tatamiyatamiyatatatamiya:20200307210752p:plain

入力のUser-Item行列と比べ,未評価で白抜きだったところがべったり埋まっています。

全体的に縦と横の縞がタータンチェックのように並んでいますが, 色の濃い横縞をたどってみると,なんとなくUser-Item行列と一致していそうに見えます。ちなみにこの濃い横筋は,低評価をつけがちなUserを表しています。

縦縞についても,movieId 27370のあたりをみると,User-Item行列とレコメンド評価値の両方に色の薄い帯が伸びているのがわかります。

これらから,各Userの平均的な評価傾向と各Itemの平均評価を混ぜて全体をならしたような出力になっていると見られそうです。

パラメータを変えた場合

では dを1~10000の範囲で動かして試してみます。

 d = 1

f:id:tatamiyatamiyatatatamiya:20200307211013p:plain

 d = 100

f:id:tatamiyatamiyatatatamiya:20200307211042p:plain

 d = 1000

f:id:tatamiyatamiyatatatamiya:20200307211102p:plain

因子数を増やすにつれて,だんだんとノイズが乗ったような図になっていきます。

ちなみに,これを10000まで増やすとこうなります。(めっちゃ時間かかった)

 d = 10000

f:id:tatamiyatamiyatatatamiya:20200307211129p:plain

だいぶ跡形がないですね。

まとめ

因子数 dの解釈

Matrix Factorizationで最適な因子数 dを選んだ場合,出力されたレコメンド評価値はユーザーとアイテムそれぞれの平均的な評価傾向を組み合わせたようなパターンを持っていました。

これは,今回扱ったFunkの方法ではUser, Itemの平均評価値に相当するバイアス項が効いているためと考えられます。

一方, dを大きくするにつれ,全体にぼやっとしたノイズが乗っていきました。

一般に,バイアス項のない通常の特異値分解では,因子数にあたるパラメータを増やすほど元の行列に近づいていき,最終的には厳密に入力と同じ値を復元できます。*3

これをもとに考えると,行列分解項 \sum_k^{d} p_{u, k} q_{k, i}は, dが大きくなるにつれ入力評価値そのものを再現する作用を強めていくと考えられます。

バイアス項がUser-Item行列を縦横方向に平滑化し,行列分解項がUserの実際の購買履歴に近づけると考えると,因子数 dは購買履歴の再現度を調整するためのパラメータと考えられるかもしれません。

正則化項について

本記事を公開時点では,通常の特異値分解と今回扱ったFunkの方法との違いを,正則化項の有無として議論していました。

しかしながら,今回扱ったライブラリscikit-surpriseの仕様を改めて確認したところ,実際の計算には正則化項は含まれておらず代わりにバイアス項が含まれていることが判明しました。

これをもとに,記事を修正いたしました。

正則化項の有無にによるレコメンド結果の差ついては,通常の特異値分解とのアルゴリズム比較と併せて別の機会に議論したいと思います。

*1:https://sifter.org/~simon/journal/20061211.html

*2:大小2種類のデータセットがありますが,評価件数100,000の小サイズの方を使います。

*3:画像の特異値分解で因子数を増やすほど元の画像が復元されていく様子を,こちらで示しています。

scikit-learnはどのようにモデルがfit済みかを確認しているか?~check_is_fittedをおちょくる暇人~

以前投稿したDecisionTreeClassifierの記事で,モデルがfit済みかを確認するcheck_is_fitted関数について少し触れました。

tatamiya-practice.hatenablog.com

この関数は,決定木に限らずscikit-learnのあらゆる実装で使われています。

そこ本記事では,check_is_fitted関数についてみていきたいと思います。

Take Home Message

check_is_fitted関数は,以下をもとにモデルがfit済みかを判定する:

  • クラスのインスタンスである
  • fitメソッドを持っている
  • 末尾or文頭が_であるattributeを持っている
    • ただし,__で始まるものは除外
続きを読む

そうだ,scikit-learnの決定木を写経しよう…!

はじめに

ブログタイトルの通り,scikit-learnの決定木の実装を読みながら,フルスクラッチで写経していこうと思います。

とはいっても,決定木の実装,めちゃくちゃムズイです。 主な理由としては,アルゴリズムのコアとなる部分の大半がCythonで実装されていることが挙げられます。 他にも,サブモジュール類が複数に分かれていて,従属関係を把握しづらい,という面もあります。

ということで,いきなりゼロからガリガリ書いたるで!っというやり方はしません。 代わりに,最初のうちはサブモジュール・基底クラスなどのかたまりはコピペ・importして使い,インターフェイス部分から徐々に掘り下げていく方針を取ろうと思います。


ということでまずは,分類木 DecisionTreeClassifierから見ていきます。

予測モデルを作る時って,だいたいいつもこんな感じでやりますよね?

from sklearn.tree import DecisionTreeClassifier
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

iris = datasets.load_iris()
X = iris.data
y = iris.target

X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y)

clf = DecisionTreeClassifier()
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

ではいきなりですが,scikit-learnのGitHubからDecisionTreeClassifierを拾って来て,動かしてみましょう!

続きを読む