pyhaya’s diary

機械学習系の記事をメインで書きます

論文の再現実験: Neural Graph Collaborative Filteringを実装してみた

本記事では「Neural Graph Collaborative Filtering (NGCF)」と呼ばれる、グラフ構造を使った推薦システムの手法を自分で実装して論文の再現実験をしてみたことについて書きます。近年の推薦システムの研究では、ユーザーとアイテムの特徴量だけでなく、ユーザーとアイテム間の関係性を考慮した手法が注目されています。NGCFもその一つで、ユーザーとアイテムのグラフ構造を学習することで、より高い精度で推薦を行うことができます。本記事では、まずNGCFの手法や特徴について解説し、その後に実装と再現実験の方法を紹介します。

Neural Graph Collaborative Filteringとは?

NGCFは、SIGIR'19 に採択された論文「Neural Graph Collaborative Filtering」で提案されました。

NGCFは、ユーザとアイテムの相互作用を表すグラフ構造を利用して推薦を行う機械学習モデルです。具体的に言うと、グラフ畳み込みネットワーク (Graph Convolutional Network, GCN) を用いて、ノード間の情報を交換することで、高度な特徴表現を学習することができます。このようにして学習された特徴表現を用いて、推薦が行われます。

NGCFの特徴

NGCFは、以下のような特徴を持っています。

  • ユーザとアイテムの関係をより詳細にモデル化できる
  • 非線形な相互作用を捉えることができる
  • ユーザとアイテムの特徴表現を同時に学習することができる

ただ、欠点として単一のユーザーとアイテム間インタラクションのみしか扱えないということがよく言われていて、最近では複数の行動を取り入れたGNNベースのモデルが発展してきています(MBGCN etc.)。

実装

github.com

実装にはPyTorchを使い、実験はNvidia Tesla T4を用いて行いました。ハイパラやデータセットは論文と同一のものを用いました。Poetryを使っている方であれば

poetry install
poetry run python main.py

だけで実行できますのでぜひ試してみてください。モデルの肝の部分は↓のように実装できます。

class NGCF(nn.Module):
    # ...
    def forward(...):
        # ...
        ego_embeddings = torch.cat(
            [self.embedding_dict["user_emb"], self.embedding_dict["item_emb"]], 0
        )
        all_embeddings = [ego_embeddings]

        for k in range(len(self.layers)):
            # Eq. (7) in the paper
            side_embeddings = torch.sparse.mm(A_hat, ego_embeddings)
            sum_embeddings = (
                torch.matmul(
                    side_embeddings + ego_embeddings, self.weight_dict[f"W_gc_{k}"]
                )
                + self.weight_dict[f"b_gc_{k}"]
            )
            bi_embeddings = torch.mul(ego_embeddings, side_embeddings)
            bi_embeddings = (
                torch.matmul(bi_embeddings, self.weight_dict[f"W_bi_{k}"])
                + self.weight_dict[f"b_bi_{k}"]
            )

            ego_embeddings = nn.LeakyReLU(negative_slope=0.2)(
                sum_embeddings + bi_embeddings
            )
            ego_embeddings = nn.Dropout(self.mess_dropout[k])(ego_embeddings)
            norm_embeddings = F.normalize(ego_embeddings, p=2, dim=1)

            all_embeddings += [norm_embeddings]

        # Eq. (9) in the paper
        all_embeddings = torch.cat(all_embeddings, 1)

ego_embedding がグラフ上を伝搬させるもので、全ユーザ・アイテムのembeddingをひとまとめにした行列になっています。A_hat はインタラクション行列から作成したラプラシアン行列 (詳しくは論文の(8)式を見てください)です。これらを使って for文の中で各層での畳込み操作をしています。最終出力で得られるユーザ・アイテムのembeddingは各層のそのユーザ・アイテムのembeddingをくっつけたものになっています。

結果

400 epochs学習させたときのRecall変化は以下のようになりました。

400 epochs後の各データセットでの指標は、

Dataset Recall@20 NDCG@20 Precision@20
Gowalla 0.1538 0.1295 0.0472
Amazon 0.0311 0.0239 0.0132

論文にはRecallとNDCGの結果しか載っていないのですが、Precisionは少し他と比べると低いようです。

また、モデルの性能を比較するためにMatrix Factorization + BPR Loss に対しても実験してみました。データセットはGowallaでbatch sizeや学習率はNGCFと同じにしました。結果 Recall@20 は 0.1400 となりNGCFよりかは9 %ほど評価値が低いという結果になりました。MFでの論文の値は Recall@20 = 0.1291 なので論文よりかは高い値が出ています。何回か実験してみるとEarly stopのかかるところが実験によって結構ばらつき、結果として評価値がそこそこブレることがわかりました。なのでEarly stopの基準を論文で設定されているものより多くすればMFの性能は論文で示されている値よりもよいものになりそうです。しかし、MFとNGCFの評価値の差は10 %近くあるのでこのブレを考慮してもNGCFの方が優れたモデルであるということはできるでしょう(ちゃんとは検証できてないですが)。

ただ、学習が進む速度は当たり前ですが遥かにMFのほうが早く、 Gowalla だと

という感じで、思ったよりNGCF遅いなといった感じでした。なので例えば実務でMFを使っていたとして、10 %精度が良くなります、でも学習時間は15倍に伸びます、CPUだともっと遅くなるのでGPUも使いたいです、はちょっとう〜んとなると思います。

感想

今回はじめてグラフ系のモデルを実装・実験してみたのですが、学習に思ったより時間がかかるという印象でした。Amazon Bookのデータセットでは400 epoch回すのに4日ほどかかりました。NGCFをより効率化したモデルであるLightGCNについても今後実装をしてみたいと思いました。

あと余談ですがNGCFはGPUメモリ思ったより使わないな、というのは結構驚きでした。↓はGowallaデータセットで訓練している最中の の結果ですが、1 GBしか使っていなかったので最初は三度見くらいしました。

+-----------------------------------------------------------------------------+
| NVIDIA-SMI 470.103.01   Driver Version: 470.103.01   CUDA Version: 11.4     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla T4            On   | 00000000:00:04.0 Off |                    0 |
| N/A   38C    P0    26W /  70W |   1090MiB / 15109MiB |      9%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A     27394      C   ...OdQNlrNe-py3.8/bin/python     1087MiB |
+-----------------------------------------------------------------------------+

おまけ


計算メモ1



計算メモ2