Skip to content

Commit 1044354

Browse files
Implement NGCF (#529)
* Generated model base from LightGCN * wip * wip example * add self-connection * refactor code * added sanity check * Changed train batch size in example to 1024 * Updated readme for example folder * Update Readme * update docs * Update block comment --------- Co-authored-by: tqtg <tuantq.vnu@gmail.com>
1 parent 5747077 commit 1044354

File tree

9 files changed

+533
-0
lines changed

9 files changed

+533
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ The recommender models supported by Cornac are listed below. Why don't you join
117117
| | [Hybrid neural recommendation with joint deep representation learning of ratings and reviews (HRDR)](cornac/models/hrdr), [paper](https://www.sciencedirect.com/science/article/abs/pii/S0925231219313207) | [requirements.txt](cornac/models/hrdr/requirements.txt) | [hrdr_example.py](examples/hrdr_example.py)
118118
| | [LightGCN: Simplifying and Powering Graph Convolution Network for Recommendation](cornac/models/lightgcn), [paper](https://arxiv.org/pdf/2002.02126.pdf) | [requirements.txt](cornac/models/lightgcn/requirements.txt) | [lightgcn_example.py](examples/lightgcn_example.py)
119119
| 2019 | [Embarrassingly Shallow Autoencoders for Sparse Data (EASEᴿ)](cornac/models/ease), [paper](https://arxiv.org/pdf/1905.03375.pdf) | N/A | [ease_movielens.py](examples/ease_movielens.py)
120+
| | [Neural Graph Collaborative Filtering](cornac/models/ngcf), [paper](https://arxiv.org/pdf/1905.08108.pdf) | [requirements.txt](cornac/models/ngcf/requirements.txt) | [ngcf_example.py](examples/ngcf_example.py)
120121
| 2018 | [Collaborative Context Poisson Factorization (C2PF)](cornac/models/c2pf), [paper](https://www.ijcai.org/proceedings/2018/0370.pdf) | N/A | [c2pf_exp.py](examples/c2pf_example.py)
121122
| | [Graph Convolutional Matrix Completion (GCMC)](cornac/models/gcmc), [paper](https://www.kdd.org/kdd2018/files/deep-learning-day/DLDay18_paper_32.pdf) | [requirements.txt](cornac/models/gcmc/requirements.txt) | [gcmc_example.py](examples/gcmc_example.py)
122123
| | [Multi-Task Explainable Recommendation (MTER)](cornac/models/mter), [paper](https://arxiv.org/pdf/1806.03568.pdf) | N/A | [mter_exp.py](examples/mter_example.py)

cornac/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from .ncf import GMF
5252
from .ncf import MLP
5353
from .ncf import NeuMF
54+
from .ngcf import NGCF
5455
from .nmf import NMF
5556
from .online_ibpr import OnlineIBPR
5657
from .pcrl import PCRL

cornac/models/ngcf/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2018 The Cornac Authors. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# ============================================================================
15+
16+
from .recom_ngcf import NGCF

cornac/models/ngcf/ngcf.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Reference: https://github.com/dmlc/dgl/blob/master/examples/pytorch/NGCF/NGCF/model.py
2+
3+
import torch
4+
import torch.nn as nn
5+
import torch.nn.functional as F
6+
import dgl
7+
import dgl.function as fn
8+
9+
10+
USER_KEY = "user"
11+
ITEM_KEY = "item"
12+
13+
14+
def construct_graph(data_set):
15+
"""
16+
Generates graph given a cornac data set
17+
18+
Parameters
19+
----------
20+
data_set : cornac.data.dataset.Dataset
21+
The data set as provided by cornac
22+
"""
23+
user_indices, item_indices, _ = data_set.uir_tuple
24+
25+
# construct graph from the train data and add self-loops
26+
user_selfs = [i for i in range(data_set.total_users)]
27+
item_selfs = [i for i in range(data_set.total_items)]
28+
29+
data_dict = {
30+
(USER_KEY, "user_self", USER_KEY): (user_selfs, user_selfs),
31+
(ITEM_KEY, "item_self", ITEM_KEY): (item_selfs, item_selfs),
32+
(USER_KEY, "user_item", ITEM_KEY): (user_indices, item_indices),
33+
(ITEM_KEY, "item_user", USER_KEY): (item_indices, user_indices),
34+
}
35+
num_dict = {USER_KEY: data_set.total_users, ITEM_KEY: data_set.total_items}
36+
37+
return dgl.heterograph(data_dict, num_nodes_dict=num_dict)
38+
39+
40+
class NGCFLayer(nn.Module):
41+
def __init__(self, in_size, out_size, norm_dict, dropout):
42+
super(NGCFLayer, self).__init__()
43+
self.in_size = in_size
44+
self.out_size = out_size
45+
46+
# weights for different types of messages
47+
self.W1 = nn.Linear(in_size, out_size, bias=True)
48+
self.W2 = nn.Linear(in_size, out_size, bias=True)
49+
50+
# leaky relu
51+
self.leaky_relu = nn.LeakyReLU(0.2)
52+
53+
# dropout layer
54+
self.dropout = nn.Dropout(dropout)
55+
56+
# initialization
57+
torch.nn.init.xavier_uniform_(self.W1.weight)
58+
torch.nn.init.constant_(self.W1.bias, 0)
59+
torch.nn.init.xavier_uniform_(self.W2.weight)
60+
torch.nn.init.constant_(self.W2.bias, 0)
61+
62+
# norm
63+
self.norm_dict = norm_dict
64+
65+
def forward(self, g, feat_dict):
66+
funcs = {} # message and reduce functions dict
67+
# for each type of edges, compute messages and reduce them all
68+
for srctype, etype, dsttype in g.canonical_etypes:
69+
if srctype == dsttype: # for self loops
70+
messages = self.W1(feat_dict[srctype])
71+
g.nodes[srctype].data[etype] = messages # store in ndata
72+
funcs[(srctype, etype, dsttype)] = (
73+
fn.copy_u(etype, "m"),
74+
fn.sum("m", "h"),
75+
) # define message and reduce functions
76+
else:
77+
src, dst = g.edges(etype=(srctype, etype, dsttype))
78+
norm = self.norm_dict[(srctype, etype, dsttype)]
79+
messages = norm * (
80+
self.W1(feat_dict[srctype][src])
81+
+ self.W2(feat_dict[srctype][src] * feat_dict[dsttype][dst])
82+
) # compute messages
83+
g.edges[(srctype, etype, dsttype)].data[
84+
etype
85+
] = messages # store in edata
86+
funcs[(srctype, etype, dsttype)] = (
87+
fn.copy_e(etype, "m"),
88+
fn.sum("m", "h"),
89+
) # define message and reduce functions
90+
91+
g.multi_update_all(
92+
funcs, "sum"
93+
) # update all, reduce by first type-wisely then across different types
94+
feature_dict = {}
95+
for ntype in g.ntypes:
96+
h = self.leaky_relu(g.nodes[ntype].data["h"]) # leaky relu
97+
h = self.dropout(h) # dropout
98+
h = F.normalize(h, dim=1, p=2) # l2 normalize
99+
feature_dict[ntype] = h
100+
return feature_dict
101+
102+
103+
class Model(nn.Module):
104+
def __init__(self, g, in_size, layer_sizes, dropout_rates, lambda_reg, device=None):
105+
super(Model, self).__init__()
106+
self.norm_dict = dict()
107+
self.lambda_reg = lambda_reg
108+
self.device = device
109+
110+
for srctype, etype, dsttype in g.canonical_etypes:
111+
src, dst = g.edges(etype=(srctype, etype, dsttype))
112+
dst_degree = g.in_degrees(
113+
dst, etype=(srctype, etype, dsttype)
114+
).float() # obtain degrees
115+
src_degree = g.out_degrees(src, etype=(srctype, etype, dsttype)).float()
116+
norm = torch.pow(src_degree * dst_degree, -0.5).unsqueeze(1) # compute norm
117+
self.norm_dict[(srctype, etype, dsttype)] = norm
118+
119+
self.layers = nn.ModuleList()
120+
121+
# sanity check, just to ensure layer sizes and dropout_rates have the same size
122+
assert len(layer_sizes) == len(dropout_rates), "'layer_sizes' and " \
123+
"'dropout_rates' must be of the same size"
124+
125+
self.layers.append(
126+
NGCFLayer(in_size, layer_sizes[0], self.norm_dict, dropout_rates[0])
127+
)
128+
self.num_layers = len(layer_sizes)
129+
for i in range(self.num_layers - 1):
130+
self.layers.append(
131+
NGCFLayer(
132+
layer_sizes[i],
133+
layer_sizes[i + 1],
134+
self.norm_dict,
135+
dropout_rates[i + 1],
136+
)
137+
)
138+
self.initializer = nn.init.xavier_uniform_
139+
140+
# embeddings for different types of nodes
141+
self.feature_dict = nn.ParameterDict(
142+
{
143+
ntype: nn.Parameter(
144+
self.initializer(torch.empty(g.num_nodes(ntype), in_size))
145+
)
146+
for ntype in g.ntypes
147+
}
148+
)
149+
150+
def forward(self, g, users=None, pos_items=None, neg_items=None):
151+
h_dict = {ntype: self.feature_dict[ntype] for ntype in g.ntypes}
152+
# obtain features of each layer and concatenate them all
153+
user_embeds = []
154+
item_embeds = []
155+
user_embeds.append(h_dict[USER_KEY])
156+
item_embeds.append(h_dict[ITEM_KEY])
157+
for layer in self.layers:
158+
h_dict = layer(g, h_dict)
159+
user_embeds.append(h_dict[USER_KEY])
160+
item_embeds.append(h_dict[ITEM_KEY])
161+
user_embd = torch.cat(user_embeds, 1)
162+
item_embd = torch.cat(item_embeds, 1)
163+
164+
u_g_embeddings = user_embd if users is None else user_embd[users, :]
165+
pos_i_g_embeddings = item_embd if pos_items is None else item_embd[pos_items, :]
166+
neg_i_g_embeddings = item_embd if neg_items is None else item_embd[neg_items, :]
167+
168+
return u_g_embeddings, pos_i_g_embeddings, neg_i_g_embeddings
169+
170+
def loss_fn(self, users, pos_items, neg_items):
171+
pos_scores = (users * pos_items).sum(1)
172+
neg_scores = (users * neg_items).sum(1)
173+
174+
bpr_loss = F.softplus(neg_scores - pos_scores).mean()
175+
reg_loss = (
176+
(1 / 2)
177+
* (
178+
torch.norm(users) ** 2
179+
+ torch.norm(pos_items) ** 2
180+
+ torch.norm(neg_items) ** 2
181+
)
182+
/ len(users)
183+
)
184+
185+
return bpr_loss + self.lambda_reg * reg_loss, bpr_loss, reg_loss

0 commit comments

Comments
 (0)