Neural Factorization Machines for Sparse Predictive Analytics (SIGIR 2017) 読んだ & Chainer で実装した
[1708.05027] Neural Factorization Machines for Sparse Predictive Analytics
みんなが好きな Factorization Machines (FM) とニューラルネットワークを組み合わせて Neural Factorization Machines (NFM) を提案する.
FM とその派生手法がいくら変数間の交互作用を表現するモデルであったとしても,しかしそれは線形モデルのままであり,表現力に乏しい,というのがモチベーション.
FM
FM は という形で予測を行う.三項目で各特徴量をで低次元表現しつつその積で交互作用を扱う,というのが FM の特徴.
NFM
NFM では として, をニューラルネットワークにすることで交互作用を考慮しつつ非線形性を表現する.
ネットワークは
- Embedding Layer
- Bi-Interaction Layer
- Hidden Layer
- Prediction Layer
の四つから構成される.三つ目は複数階層のニューラルネットワーク,四つ目はただの 1 次元に落とす操作なので前二つについて書く.
Embedding Layer
FM や NFM が扱うのはたいていスパースなので,値がある次元をそれぞれ低次元に Embedding する.例えば, D 次元の入力のうち N 個だけが値を持っているとする時,K 次元に埋め込むとすると K 次元のベクトルが N 本作られる.またこの時,各次元の値を埋め込んだベクトルに掛けることを忘れないようにする.
Bi-Interaction Layer
続いて交互作用を考える. Embedding Layer から届くのは K 次元の N 本のベクトルなので,これを K 次元の一本のベクトルに変換したい.何も考えずにやると average pooling とか max pooling とかやる(例えば Learning Hierarchical Representation Model for Next Basket Recommendation (SIGIR 2015) 読んだ - 糞ネット弁慶 など.論文でも言及済)わけですが,ここでは陽に交互作用を考える.
すなわち, とする.ここで は埋め込みベクトルに元の特徴量をかけた K 次元のベクトルで, はベクトルの要素ごとの積.
これは としても計算ができる.
あとは batch normalization や dropout や residual unit なんかを Hidden Layer で入れることでモダンな感じになる.
実装
Chainer でやった.コードはこの gist にアップロードした.
ファイルは libsvm format で入力すると動作する.ひとまず classification がやりたかったので最後に sigmoid に通している.
「埋め込みを行いつつ元の値を掛ける」という操作をどうやっていいのかわからなかったので
- 入力を「発火している特徴量の id を並べ,それ以外は -1 で padding したリスト」と「そのリストの各要素に対応する元の特徴量の値」に分ける
- 特に後者は埋め込み後に掛ける必要があるので埋め込み次元に引き伸ばす
という方針でやる.
例えば特徴量が {0: 0.1, 3: 3, 7: 1} であり,特徴量の最大次元が10であり,3次元に埋め込む場合は
raw_fv = {0: 0.1, 3: 3, 7: 1} # こんな感じで入力を書く active_feature_ids = chainer.Variable(np.array([0, 3, 7, -1, -1, -1, -1, -1, -1, -1], dtype=np.int32)) feature_weights = chainer.Variable(np.array([[0.1, 0.1, 0.1], [3, 3, 3], [1, 1, 1], [0, 0, 0], ...], dtype=np.float32)) # 中略 # chainer の predict にて # 'EMB': L.EmbedID(10, 3, ignore_label=-1) として layer を定義しておく # まずは埋め込む emb = self.EMB(active_feature_ids) # 埋め込んだベクトルに元の特徴量の値をかける emb = emb * feature_weights # 組み合わせを考える pairwise = F.square(F.sum(emb, axis=1)) - F.sum(F.square(emb), axis=1) pairwise *= 0.5 # あとは何層か重ねる pairwise = F.relu(self.l_pairwise_1(pairwise)) pairwise = F.relu(self.l_pairwise_2(pairwise)) pairwise = F.relu(self.l_pairwise_3(pairwise))
という感じで書いている.もっと綺麗に書く方法があると思うが書き慣れていないのでよくわかっていない.
これに加えてニューラルでない線形和の部分も chainer で推定させるために愚直にスパースな特徴ベクトルも渡している.綺麗に書きたい.
movielens のデータではそれらしく動いているのであとでちゃんと検証する.