LightFMをMovielensに適用してみた


Factoriazation Machines関連のライブラリを調査している過程で、LightFMというライブラリに出会ったので使ってみました。

最終的には自作のデータセットに適用してみたいのですが、今回はLightFMの使い方に慣れるのをかねてMovielens(映画レコメンデーションデータセット)に適用してみます。

LightFMリポジトリ
[GitHub - lyst/lightfm: A Python implementation of LightFM, a hybrid recommendation algorithm.](https://github.com/lyst/lightfm)


# LightFMとFMの違い

LightFMは、FMとついているのですがFactorization Machinesのライブラリではありません。

LightFMの[論文](https://arxiv.org/abs/1507.08439)を読んでみると、著者らは「LightFMはFMの特別な場合」と述べています。

FMのpython実装を探していたので、多少がっかりしたものの、読んでみるとこれでも自分の試したいタスクを解けそうなモデルをしていたのでこちらを試してみようと思いました。(あとチュートリアルやドキュメントが充実している)


通常のFMはuser id, item idの他にcontext featureとしていろんな特徴を入力に用いることができ、それぞれembeddingをとり全ての内積とって和を出力とします。


```math
f(x)=w_0 + \sum_{i=1}^d w_ix_i + \sum_{i,j}\langle \boldsymbol v_i, \boldsymbol 
 v_j \rangle x_ix_j
```


user embeddingとitem embeddingの内積、context featureどうしの内積など、すべてとります。


LightFMは、FMのようにcontext featureを入れることは部分的に可能ですが、なんでもかんでも入れることができるわけではありません。

追加する特徴は、userの特徴かitemの特徴かのどちらか、という縛りがあります。

内積も、userの特徴はitemの特徴としか内積をとりません。userの特徴どうしの内積をとりません。

```math
f(i, u)=  \langle \boldsymbol p_i,  \boldsymbol q_u \rangle + b_i +b_u 
```
ただし


```math
　\boldsymbol p_i　= \sum_{j \in f_i}\boldsymbol e_{j}^I
```

```math
  \boldsymbol q_u　= \sum_{j \in f_u}\boldsymbol e_{j}^U
```

```math
  b_i　= \sum_{j \in f_i}b_{j}^U
```

```math
  b_u　= \sum_{j \in f_u} b_{j}^U
```

です。


userやitemの特徴を与えない場合、Matrix Factorizationの形に一致します。

LightFMがサポートしているlossは、BPRとWARP、warp-kos, logistic lossです。前者3つはランキングに対するlossで、implicit feedbackでランキング学習をするのに適しています。

BPRについてはこちらに記事を書いたのでよろしければどうぞ！

[【論文紹介】BPR: Bayesian Personalized Ranking from Implicit Feedback (UAI 2009)](https://www.smartbowwow.com/2019/09/bpr-bayesian-personalized-ranking-from.html)

# 動かしてみよう

## Movielensデータセット取得

Movielensは映画レコメンデーションのデータセットで、この分野ではよく実験で用いられています。
今回は比較的小さいサイズに絞って動かしています。


LightFMは親切にもMovielensをロードするクラスを提供しているので、これを使ってみます。

(LightFMのinstallはpipでもcondaでも入ります。省略)

```python
from lightfm.datasets import fetch_movielens
data = fetch_movielens()

data.keys()
```
とすると

```
dict_keys(["train", "test",  "item_features", "item_feature_labels", "item_labels"])
```

がかえってきました。

trainとtestは、(n_user,n_item)の行列でratingが入っています。ratingが付いてないのは0が入っています。

item_featuresは、(n_item, n_features)の行列で各itemがどの特徴を持っているかを01で表したもの...と思ったのですが、中身を見てみると正方行列でしかも単位行列っぽいです。

また、item_feature_labelは各featureの名前、item_labelsはitemの名前が入っている、とドキュメントにはあり、それぞれ(n_feature,) , (n_item,)のarrayであると書いてあるのですが、中身を見てみるとどちらも

```python
array(['Toy Story (1995)', 'GoldenEye (1995)', 'Four Rooms (1995)', ...,
       'Sliding Doors (1998)', 'You So Crazy (1994)',
       'Scream of Stone (Schrei aus Stein) (1991)'], dtype=object)
```

でした。すべてitemの名前です。

おそらくこのチュートリアルではitem featureは使わないで(つまりMatrix Factorizationで)動かしてみよう、ということですかね。

## 学習部分

実際にモデルを学習させる部分はこちら。

```python
train = data["train"]
test = data["test"]

model = LightFM(no_components=10,learning_rate=0.05, loss='bpr')
model.fit(train, epochs=10)

train_precision = precision_at_k(model, train, k=10).mean()
test_precision = precision_at_k(model, test, k=10).mean()

train_auc = auc_score(model, train).mean()
test_auc = auc_score(model, test).mean()

print('Precision: train %.2f, test %.2f.' % (train_precision, test_precision))
print('AUC: train %.2f, test %.2f.' % (train_auc, test_auc))
```

`no_components`でembeddingの次元を指定できます。今回は10。



modelのfitやaucのところで、

```python
model.fit(train, item_features=data["item_features"], epochs=10)
auc_score(model, train, item_features=data["item_features"]).mean()
```
などとするとitem_featuresを入れることができます。

ただ、今回は入れても入れなくてもitem_featuresはitem idしか入っていないので結果は変わらないはず。

```
item_featureなし
Precision: train 0.59, test 0.10.
AUC: train 0.90, test 0.86.

item_featureあり
Precision: train 0.59, test 0.10.
AUC: train 0.89, test 0.86.
```

と思ったのですが、微妙に違いますね...
誤差でしょうか。

LightFMのソースを見に行くと、item_featuresが指定されていないと、(n_item,n_item)の単位行列でitem featuresを作っていたので、上記の両方で同じ挙動になるはずですが。

## embedding取得


学習後のembeddingは、以下のようにして取得できます。

```python
user_embedding=model.user_embeddings
item_embedding=model.item_embeddings
```

user_embeddingは(n_user_features, no_components)
item_embeddingは(n_item_features, no_components)

のarrayです。

先ほどソースを見に行ってわかった通り、featuresを指定しない場合はuser/item id分のembeddingが得られていました。


## predict

最後、predictのところが少しつまづきポイントがあったのでメモしておきます。

user_idとitem_idを与えると、その組についてスコアを計算してくれます。
idの与え方は、まずuser一人あたりにitem idを複数与える場合があります。


```python
model.predict(user_ids=0,item_ids=[1,3,4])
```

とすると、user0さんのitem1,3,4に対するスコアが計算されます。
userごとにrankingを作る場面が多いと思うので、この書き方は便利ですね。

複数のuser idと複数のitem idの組について少し注意すべきポイントがあって、


```python
model.predict(user_ids=[4,3,1],item_ids=[1,3,4])
```
とやるとAssertion Errorが出ます。

正しくは

```python
import numpy as np
model.predict(user_ids=np.array([4,3,1]),item_ids=[1,3,4])
```

ソースを読んでみると、最初にuser idがnumpy arrayでない場合はitem idと個数を揃えるためにitem idの個数分repeatする処理が入ります。
user idがintの場合(先ほど書いたuser一人当たりに複数itemに対してスコアを計算する場合)にはこれで長さが揃うのですが、user_idsをlistで与えた時もrepeatされて長さが揃わなくなります。これは罠...


「user_idsがlenを計算できるか」とかでrepeatするかを決めればいいのに。リスト与えちゃう人いるでしょ...

## lossについて

BPRやWARPは、ratingが正のものをpositive feedbackと捉えるような実装になってました。こちらで1に直して与える、とかはしなくてもよいみたいです。

logistic lossは、+1と-1のfeedbackがあるときに使えます、と書いてありましたが、
そのままratingの行列を与えても動きました。(内部で何をしているのかはソースを眺めただけではちょっとよくわかりませんでした)


```:logistic
Precision: train 0.43, test 0.08.
AUC: train 0.87, test 0.84.
```

```:bpr(再掲)
Precision: train 0.59, test 0.10.
AUC: train 0.90, test 0.86.
```

ただランキングの指標には適していないことがわかります。


# おわりに

以上、これで基本的な処理は行えるようになったかと思います。

チュートリアルやドキュメントがわかりやすいこと、サポートされているlossが複数あるのは嬉しいです。

さらっと書いてしまいましたが、AUCやPrecision@kなどの評価指標も提供されているのも便利ですね。

次はいよいよ自作のデータセットに対してLightFMを適用してみようと思います!

最後まで読んでいただきありがとうございました。