cf.

  • https://qiita.com/s_katagiri/items/d630685dd2fe2627ecd3
  • https://blog.recruit.co.jp/rco/47/

概要

Factorization Machine (FM) は、ユーザー属性とアイテム評価、さらにはその他の属性情報も組み合わせ推薦アルゴリズムで、Matrix Factorization の発展系。

2010年に Rendle が発表。

理論

基本的な協調フィルタリングや Matrix Factorization では、ユーザ $u_i$ がアイテム $v_j$ を評価した値 $r_{i,j}$ を並べた以下のような評価値テーブルを使っていた。

  $v_1$ $v_2$ $v_3$ $v_4$
$u_1$ 0.5   0.2 0.8
$u_2$   0.2   0.6
$u_3$     0.3  

対して Factorization Machine では、評価値 $r_{i,j}$ それぞれを1行に対応させ、ユーザ $u_i$ やアイテム $v_j$、その他の特徴量 $a_l$ を列に持つ以下のようなテーブルを考える:

  $u_1$ $u_2$ $u_3$ $v_1$ $v_2$ $v_3$ $v_4$ $a_1$ $a_2$ $\cdots$
$\boldsymbol{x}^{(1)}(r_{1,1}=0.5)$ 1 0 0 1 0 0 0 3.1 0.3  
$\boldsymbol{x}^{(2)}(r_{1,3}=0.2)$ 1 0 0 0 0 1 0 2.5 -0.2  
$\boldsymbol{x}^{(3)}(r_{1,4}=0.8)$ 1 0 0 0 0 0 1 1.3 0.9  
$\boldsymbol{x}^{(4)}(r_{2,2}=0.2)$ 0 1 0 0 1 0 0 3.0 0.8  
$\boldsymbol{x}^{(5)}(r_{2,4}=0.6)$ 0 1 0 0 0 0 1 0.9 -0.4  
$\boldsymbol{x}^{(6)}(r_{3,3}=0.3)$ 0 0 1 0 0 1 0 1.7 0.3  

$u_i, v_j$ の列は、値が1であれば、そのユーザやアイテムに関する評価値であることを示す。
特徴量 $a_l$ は何でも良い:

  • ユーザ属性:年齢 / 性別 / 自身で登録してもらった好きなジャンル
  • アイテム属性:ジャンル / 価格 / 口コミ数
  • ユーザ-アイテム間の関係:そのユーザがそのアイテムを閲覧 / 購入 / お気に入り登録

$u_i, v_j, a_l$ 全てを含めた特徴量の数(上の行列の列数)を $n$ として、$l$ 番目の評価値 $r^{(l)}$ の推定値 $\hat{r}^{(l)}$ を以下の式で計算する。

\[\hat{r}^{(l)} = w_0 + \sum_{i=1}^n w_i x_i^{(l)} + \sum_{i=1}^n \sum_{j=i+1}^n (\boldsymbol{f}_i \cdot \boldsymbol{f}_j) x_i^{(l)} x_j^{(l)} \tag{1}\]

ただし、

\[w_0 \in \mathbb{R},\quad \boldsymbol{w} \in \mathbb{R}^n,\quad \boldsymbol{f}_1, \cdots, \boldsymbol{f}_n \in \mathbb{R}^k\]

は学習により最適化するモデル変数であり、

  • $w_0$:定数項
  • $\boldsymbol{w}$:各特徴量ごとの独立した重み
  • $\boldsymbol{f}_i$:各特徴量ごとの重みで、特徴量間の相互作用を考えるため内積を取って使う
    • $k$ は $\boldsymbol{f}_i$ の次元を決めるハイパーパラメータで、$k \lt n$

$(1)$ を愚直に計算すると相互作用の項が $O(kn^2)$ の計算量になるが、式変形により

\[\begin{eqnarray} \sum_{i=1}^n \sum_{j=i+1}^n (\boldsymbol{f}_i \cdot \boldsymbol{f}_j) x_i^{(l)} x_j^{(l)} &=& \cfrac{1}{2} \sum_{i=1}^n \sum_{j=1}^n (\boldsymbol{f}_i \cdot \boldsymbol{f}_j) x_i^{(l)} x_j^{(l)} - \cfrac{1}{2} \sum_{i=1}^n (\boldsymbol{f}_i \cdot \boldsymbol{f}_i) (x_i^{(l)})^2 \\ &=& \cfrac{1}{2} \sum_{i=1}^n \sum_{j=1}^n \sum_{p=1}^k f_{ip} f_{jp} x_i^{(l)} x_j^{(l)} - \cfrac{1}{2} \sum_{i=1}^n \sum_{p=1}^k f_{ip}^2 (x_i^{(l)})^2 \\ &=& \cfrac{1}{2} \sum_{p=1}^k \left( \sum_{i=1}^n f_{ip} x_i^{(l)} \right) \left( \sum_{j=1}^n f_{jp} x_j^{(l)} \right) - \cfrac{1}{2} \sum_{i=1}^n \sum_{p=1}^k \left(f_{ip} x_i^{(l)}\right)^2 \\ &=& \cfrac{1}{2} \sum_{p=1}^k \left( \sum_{i=1}^n f_{ip} x_i^{(l)} \right)^2 - \cfrac{1}{2} \sum_{i=1}^n \sum_{p=1}^k \left(f_{ip} x_i^{(l)}\right)^2 \\ &=& \cfrac{1}{2} \sum_{p=1}^k \left\{ \left( \sum_{i=1}^n f_{ip} x_i^{(l)} \right)^2 - \sum_{i=1}^n \left(f_{ip} x_i^{(l)}\right)^2 \right\} \tag{2} \end{eqnarray}\]

とすれば、計算量は $O(kn)$ に抑えられる。

損失関数 $L$ は予測値と実際の評価値の誤差平方和:

\[L := \sum_{l=1}^m \left\{ \cfrac{1}{2} \left(\hat{r}^{(l)} - r^{(l)} \right)^2 \right\} + \cfrac{\lambda}{2} \left( w_0^2 + \sum_{i=1}^n w_i^2 + \sum_{i=1}^n \vert \boldsymbol{f}_i \vert^2 \right) \tag{3}\]

第二項は正則化項で、$\lambda$ はそのためのハイパーパラメータ。勾配計算の係数調整のため $1/2$ をかけてある。

\[\begin{eqnarray} \cfrac{\partial L}{\partial w_0} &=& \sum_{l=1}^m \left\{ \cfrac{\partial \hat{r}^{(l)}}{\partial w_0} \left(\hat{r}^{(l)} - r^{(l)} \right) \right\} + \lambda w_0 \\ &=& \sum_{l=1}^m \left(\hat{r}^{(l)} - r^{(l)} \right) + \lambda w_0 \tag{4} \\ \\ \cfrac{\partial L}{\partial w_i} &=& \sum_{l=1}^m \left\{ \cfrac{\partial \hat{r}^{(l)}}{\partial w_i} \left(\hat{r}^{(l)} - r^{(l)} \right) \right\} + \lambda w_i \\ &=& \sum_{l=1}^m x_i^{(l)} \left(\hat{r}^{(l)} - r^{(l)} \right) + \lambda w_i \tag{5} \\ \\ \cfrac{\partial L}{\partial f_{ij}} &=& \sum_{l=1}^m \left\{ \cfrac{\partial \hat{r}^{(l)}}{\partial f_{ij}} \left(\hat{r}^{(l)} - r^{(l)} \right) \right\} + \lambda f_{ij} \\ &=& \sum_{l=1}^m \left( x_i^{(l)} \sum_{p=1}^n x_p^{(l)} f_{pj} - (x_i^{(l)})^2 f_{ij} \right) \left(\hat{r}^{(l)} - r^{(l)} \right) + \lambda f_{ij} \tag{6} \end{eqnarray}\]

より、勾配降下法などを用いてパラメータを最適化する。

Factorization Machine の強み

$n$ 個の特徴量間の相互作用の重みとしては、$n\times n$ 要素のパラメータ行列 $W_{ij}$ を考えるのが素直:

\[\hat{r}^{(l)} = w_0 + \sum_{i=1}^n w_i x_i^{(l)} + \sum_{i=1}^n \sum_{j=i+1}^n W_{ij} x_i^{(l)} x_j^{(l)}\]

Factorization Machine では、この $W_{ij}$を特徴量ごとの $k < n$ 次元ベクトル $\boldsymbol{f}_1,\cdots,\boldsymbol{f}_n$ の内積 $\boldsymbol{f}_i \cdot \boldsymbol{f}_j$ に置き換えた形になっている。

強み1:計算量の節約

  • $W_{ij}$ の変数は $n^2$ 個
  • $\boldsymbol{f}_1,\cdots,\boldsymbol{f}_n$ の変数は $nk$ 個

なので、$k \lt n$ よりパラメータ数を小さく抑えることができる。

強み2:疎なデータへの耐性

ユーザやアイテムは one-hot ベクトルとして表現されている。これに限らず、カテゴリデータを one-hot 化した特徴量を使いたいことも多い。
こういった場合、特徴量ベクトル $\boldsymbol{x}$ が疎になるため、$x_i,x_j$ いずれかが0になる(つまり、相互作用項にあたる $x_i x_j$ が0になる)確率が高く、まともに $W_{ij}$ を学習できない。
特に、one-hot 化されているユーザ同士、アイテム同士、カテゴリ同士では必ず $x_i x_j = 0$ となってしまう(いずれか1つの成分を除いて0となるため)。

一方で特徴量ベクトルの内積 $\boldsymbol{f}_i \cdot \boldsymbol{f}_j$ を用いる場合は、常に $x_i x_j = 0$ であっても、どこかで $x_i x_k \ne 0$ となる $k(\ne j)$ があれば、$x_i$ に対応するベクトル $\boldsymbol{f}_i$ を学習できる。
同じことは $\boldsymbol{f}_j$ にも言えるので、他の特徴量を用いて学習した $\boldsymbol{f}_i, \boldsymbol{f}_j$ を用いて、間接的に $x_i, x_j$ の相互作用を求めることができる。

実装 (ToDo)

import numpy as np
from matplotlib import pyplot as plt
import time

class FactorizationMachine:
    def __init__(self):
        pass



df_movies = pd.read_csv('ml-latest-small/movies.csv', header=0)
df_rating = pd.read_csv('ml-latest-small/ratings.csv', header=0)

idx2genre = []
genre2idx = {}
mid2genres = {}
for _, row in df_movies.iterrows():
    mid = int(row['movieId'])
    gs = row['genres']
    mid2genres[mid] = []
    for g in gs.split('|'):
        if g not in genre2idx:
            genre2idx[g] = len(idx2genre)
            idx2genre.append(g)
        mid2genres[mid].append(g)

n_genre = len(idx2genre)


# MatrixFactorization に渡す入力と正解
X_fm = np.full((len(df_rating), n_user+n_movie+n_genre), 0, dtype=float)
Y_fm = np.full(len(df_rating), 0, dtype=float)
for i, row in df_rating.iterrows():
    idx_u = uid2idx[int(row['userId'])]
    idx_m = mid2idx[int(row['movieId'])]
    X_fm[i, idx_u] = 1
    offset = n_user
    X_fm[i, offset+idx_m] = 1
    offset += n_movie
    for g in mid2genres[int(row['movieId'])]:
        idx_g = genre2idx[g]
        X_fm[i, offset+idx_g] = 1
    Y_fm[i] = row['rating']