Neural Collaborative Filtering を概観する

# Neural Collaborative Filtering とは
・Matrix Factorization(MF)の因子行列の積計算をニューラルネットワークとして表現したもの

## Matrix Factorization (MF) のおさらい
評価値行列をユーザー因子行列とアイテム因子行列の積で近似することで予測評価値を得るモデルベースの推薦アルゴリズム。
### MFの特徴
・協調ベースであるため多様な推薦ができる。
・線形かつ評価値のある組み合わせだけで計算可能なので計算が重くない
・評価値がないor少ないアイテム、ユーザーに対して予測しきれない（コールドスタート問題）
など

![weE1qz6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2363786/67469129-8ad2-48c3-2b42-412cca670c96.png)

この画像での「K」(因子数)はハイパーパラメーターになります。因子数の数に依存せず、行列積の大きさは、評価値行列の大きさと同じになります。なので、因子数を大きくすれば表現力が増すのでモデルの精度が増します。しかし、過学習するので(評価数が少なく評価値が大きいアイテムに対してモデルが過剰に反応する)、ユーザー、アイテム因子行列を分散的に表す必要があります。一般的には、L2正則化項を損失関数に加えることで、汎化性能を保つことができます。

$$
\hat{r}_{ui}=p_u^Tq_i\qquad(1)
$$
予測評価値とベクトルの積を(1)のように定義できます。そして、損失関数は、

$$
min_{P,Q}\sum_{(u,i)\in K}(r_{ui}-q_i^Tp_u)^2+\lambda(||q_i||^2+||p_u||^2)\qquad(2)
$$
となります。1項目で評価値と予測評価値の二乗誤差を出します。二項目は、L2正則化を加え上記で説明したように過剰に大きくなるベクトルをペナルティ項としてあげます。最適化計算には、SGDやAdamなりを適用させて因子行列を更新していくとしましょう。


## Neural Collaborative Filtering
結論から言うと、$p_u^Tq_i$が全結合のニューラルネットワークに化けるという感じでしょうか。

### 図で説明してみる

(1)一つのアイテム（緑色）、一つのユーザー(青色)ベクトルを因子行列から取り出す。（因子行列の画像は上のと同じです！）
![weE1qz6 2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2363786/28b9f732-0bde-bf05-6cff-3ec595c72f18.jpeg)
(2)それぞれのベクトルを横並びにする。
(3)順伝播開始
![neural_cf.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2363786/c1af435c-17df-d0f9-e8ad-a2b2ecfc41b8.jpeg)
(4)一番上の赤丸部分、$\hat{y}_{ui}$がある一人のある一つのアイテムに対する予測評価値になります。MFモデルでは$p_u^Tq_i$とベクトルの積で予測値を出していたのに対し、NNモデルに拡張したらいくつもの関数が合成され評価値を得ることになります。

(5)そして、評価値行列に対応する$y_{ui}$と$\hat{y}_{ui}$の損失を計算していきます。L2正則化項を加えたりして。この辺は、MFモデルの損失計算と同じだと思います。しかし、NNモデルなので中間層のパラメータにも目を配らないといけません。何層のネットにするのか？活性化関数はどうするのか？などなど。（私も勉強中なのであまり深入りできません。。）

(6)次の対のベクトルを取り出し、(2)以下を繰り返していく。

### サッと実装してみる
pytorch, numpyを使っていきます。
```python
import torch
from torch import nn, optim
import numpy as np

# ユーザー数、アイテム数、評価値の範囲
num_users = 50
num_items = 50
max_rating = 5
min_rating = 1

# データセットの生成
ratings = np.random.randint(min_rating, max_rating+1, size=(num_users, num_items))
ratings = (ratings-1)/4

# ネットワークの定義
class NCF(nn.Module):
    def __init__(self, num_users, num_items, factor_num=5, layers=[10,5]):
        super(NCF, self).__init__()
        self.num_users = num_users
        self.num_items = num_items
        self.factor_num = factor_num
        self.layers = layers

        self.embedding_user = nn.Embedding(num_embeddings=self.num_users, embedding_dim=5)
        self.embedding_item = nn.Embedding(num_embeddings=self.num_items, embedding_dim=5)

        self.fc_layers = nn.ModuleList()
        for idx, (in_size, out_size) in enumerate(zip(self.layers[:-1], self.layers[1:])):
            self.fc_layers.append(nn.Linear(in_size, out_size))

        self.affine_output = nn.Linear(in_features=self.layers[-1], out_features=1)
        self.logistic = nn.Sigmoid()

    def forward(self, user_indices, item_indices):
        user_embedding = self.embedding_user(user_indices)
        item_embedding = self.embedding_item(item_indices)
        vector = torch.cat([user_embedding, item_embedding], dim=-1)  # the concat latent vector
        for idx, _ in enumerate(range(len(self.fc_layers))):
            vector = self.fc_layers[idx](vector)
            vector = nn.ReLU()(vector)
        logits = self.affine_output(vector)
        rating = self.logistic(logits)
        return rating
```
`ratings`変数に50x50の評価値行列を用意します。ここでは、アルゴリズムの概観を掴むため非現実的な設定をしています。一つが50x50という小規模マーケットであること。もう一つが評価値行列が真の評価値という設定をしていること。つまり、未知なる評価値を当てるのではなく、既知な評価値を近似するようなモデルを作るとします。

pytorchなじみの、`nn.Module`クラスをオーバーライドして、Neural Collaborative Filteringモデルクラス`NCF`を作っていきます。コンストラクタで色々入れていきます。因子数`factor_num`は5とします。層数`layers`は[10, 5]とします。一層目は、10です。これは、因子数に依存します。なぜなら、一人のユーザー因子ベクトルと一つのアイテム因子ベクトルを特徴量として、順伝播を始めるためです。(上の方で説明した「(2)それぞれのベクトルを横並びにする」のところ！縦並びでもいいのだがとりあえず分かりやすくするために)今回、因子数を5にしたので、5x2で一層目は、10になります。そして、二層目は5層。そこを通過し、`self.affine_output`として一つの値を得ます。そして、シグモイド関数を通り、予測評価値になるという具合です。(forwardメソッド内の話)予測評価値を0-1スケールで出すため、評価値行列`ratings`はスケール変換してます。

```python
model = NCF(num_users, num_items)
loss_function = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.0001)

losses = []
for epoch in range(10):
    model.train() # Enable dropout (if have).
    
    epoch_loss = 0.0
    for user_index in range(num_users):
        for item_index in range(num_items):
            rating = float(ratings[user_index, item_index])
            label = torch.FloatTensor([[rating]])
        
            optimizer.zero_grad()
            user = torch.LongTensor([user_index])
            item = torch.LongTensor([item_index])
            prediction = model(user, item)
            
            loss = loss_function(prediction, label)
            loss.backward()
            optimizer.step()
            epoch_loss+=loss.item()
    
    losses.append(epoch_loss/(num_users*num_items))
    model.eval()

losses
[0.6988408488035202,
 0.6969971101880074,
 0.6958758408546448,
 0.6950538464546203,
 0.6943823150038719,
 0.6938250337600708,
 0.693351780462265,
 0.6929443723917007,
 0.6925905404210091,
 0.6922729956626892]
```
損失関数にクロスエントロピー、最適化計算にはAdamを設定しました。10エポックでそれぞれの因子ベクトルを取り出し、更新してきます。ループ毎に型変換してたり、もっと良い書き方がありそうです。そして、エポック毎の損失の平均を`losses`にappendしてます。


## まとめ

Matrix factorizationの線形計算がNNモデルに置き換わったNeural Collaborative Filteringを見てきました。今回はアルゴリズムの違いをアウトプットするためモデルの精度まで追求するのではなく、概観することにしました。なぜなら、「よいモデル」はビジネス設計に依存してくるためです。得られるデータ構造は？KPIは？どこのエンドポイントに向けたAPI?など課題設計によって目的関数、評価指標も変わってくるはず。(「施策デザインのための機械学習入門」と「評価指標入門」という本がすごく参考になった！)

また、NNモデルがいつでも有用かというとそうでもないことがこの記事からわかりました。 線形モデルをもっと解剖していけば有用なケースが十分にあるということですね。

https://developers.cyberagent.co.jp/blog/archives/38632/

NNモデルを使った推薦システムを初めて触ってみて感覚はわかってきた（はず）。次はtwo-tower-modelを概観したい。


### 参考資料

・ 施策デザインのための機械学習入門: https://gihyo.jp/book/2021/978-4-297-12224-9
・ 評価指標入門:　https://gihyo.jp/book/2023/978-4-297-13314-6
・ CyberAgent Developers Blog:　https://developers.cyberagent.co.jp/blog/archives/38632/