Coverage for hopwise/model/general_recommender/ngcf.py: 90%
81 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 13:25 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 13:25 +0000
1# @Time : 2020/7/16
2# @Author : Zihan Lin
3# @Email : linzihan.super@foxmail.com
5# UPDATE:
6# @Time : 2020/9/16
7# @Author : Shanlei Mu
8# @Email : slmu@ruc.edu.cn
10r"""NGCF
11################################################
12Reference:
13 Xiang Wang et al. "Neural Graph Collaborative Filtering." in SIGIR 2019.
15Reference code:
16 https://github.com/xiangwang1223/neural_graph_collaborative_filtering
18"""
20import torch
21import torch.nn.functional as F
22from torch import nn
24from hopwise.model.abstract_recommender import GeneralRecommender
25from hopwise.model.init import xavier_normal_initialization
26from hopwise.model.layers import BiGNNLayer, SparseDropout
27from hopwise.model.loss import BPRLoss, EmbLoss
28from hopwise.utils import InputType
31class NGCF(GeneralRecommender):
32 r"""NGCF is a model that incorporate GNN for recommendation.
33 We implement the model following the original author with a pairwise training mode.
34 """
36 input_type = InputType.PAIRWISE
38 def __init__(self, config, dataset):
39 super().__init__(config, dataset)
41 # load parameters info
42 self.embedding_size = config["embedding_size"]
43 self.hidden_size_list = config["hidden_size_list"]
44 self.hidden_size_list = [self.embedding_size] + self.hidden_size_list
45 self.node_dropout = config["node_dropout"]
46 self.message_dropout = config["message_dropout"]
47 self.reg_weight = config["reg_weight"]
49 # define layers and loss
50 self.sparse_dropout = SparseDropout(self.node_dropout)
51 self.user_embedding = nn.Embedding(self.n_users, self.embedding_size)
52 self.item_embedding = nn.Embedding(self.n_items, self.embedding_size)
53 self.emb_dropout = nn.Dropout(self.message_dropout)
54 self.GNNlayers = torch.nn.ModuleList()
55 for idx, (input_size, output_size) in enumerate(zip(self.hidden_size_list[:-1], self.hidden_size_list[1:])):
56 self.GNNlayers.append(BiGNNLayer(input_size, output_size))
57 self.mf_loss = BPRLoss()
58 self.reg_loss = EmbLoss()
60 # storage variables for full sort evaluation acceleration
61 self.restore_user_e = None
62 self.restore_item_e = None
64 # generate intermediate data
65 self.norm_adj_matrix = dataset.norm_adjacency_matrix(form="torch.sparse").to(self.device)
66 self.eye_matrix = dataset.eye_matrix(form="torch.sparse").to(self.device)
68 # parameters initialization
69 self.apply(xavier_normal_initialization)
70 self.other_parameter_name = ["restore_user_e", "restore_item_e"]
72 def get_ego_embeddings(self):
73 r"""Get the embedding of users and items and combine to an embedding matrix.
75 Returns:
76 Tensor of the embedding matrix. Shape of (n_items+n_users, embedding_dim)
77 """
78 user_embeddings = self.user_embedding.weight
79 item_embeddings = self.item_embedding.weight
80 ego_embeddings = torch.cat([user_embeddings, item_embeddings], dim=0)
81 return ego_embeddings
83 def forward(self):
84 A_hat = self.sparse_dropout(self.norm_adj_matrix) if self.node_dropout != 0 else self.norm_adj_matrix
85 all_embeddings = self.get_ego_embeddings()
86 embeddings_list = [all_embeddings]
87 for gnn in self.GNNlayers:
88 all_embeddings = gnn(A_hat, self.eye_matrix, all_embeddings)
89 all_embeddings = nn.LeakyReLU(negative_slope=0.2)(all_embeddings)
90 all_embeddings = self.emb_dropout(all_embeddings)
91 all_embeddings = F.normalize(all_embeddings, p=2, dim=1)
92 embeddings_list += [all_embeddings] # storage output embedding of each layer
93 ngcf_all_embeddings = torch.cat(embeddings_list, dim=1)
95 user_all_embeddings, item_all_embeddings = torch.split(ngcf_all_embeddings, [self.n_users, self.n_items])
97 return user_all_embeddings, item_all_embeddings
99 def calculate_loss(self, interaction):
100 # clear the storage variable when training
101 if self.restore_user_e is not None or self.restore_item_e is not None:
102 self.restore_user_e, self.restore_item_e = None, None
104 user = interaction[self.USER_ID]
105 pos_item = interaction[self.ITEM_ID]
106 neg_item = interaction[self.NEG_ITEM_ID]
108 user_all_embeddings, item_all_embeddings = self.forward()
109 u_embeddings = user_all_embeddings[user]
110 pos_embeddings = item_all_embeddings[pos_item]
111 neg_embeddings = item_all_embeddings[neg_item]
113 pos_scores = torch.mul(u_embeddings, pos_embeddings).sum(dim=1)
114 neg_scores = torch.mul(u_embeddings, neg_embeddings).sum(dim=1)
115 mf_loss = self.mf_loss(pos_scores, neg_scores) # calculate BPR Loss
117 reg_loss = self.reg_loss(u_embeddings, pos_embeddings, neg_embeddings) # L2 regularization of embeddings
119 return mf_loss + self.reg_weight * reg_loss
121 def predict(self, interaction):
122 user = interaction[self.USER_ID]
123 item = interaction[self.ITEM_ID]
125 user_all_embeddings, item_all_embeddings = self.forward()
127 u_embeddings = user_all_embeddings[user]
128 i_embeddings = item_all_embeddings[item]
129 scores = torch.mul(u_embeddings, i_embeddings).sum(dim=1)
130 return scores
132 def full_sort_predict(self, interaction):
133 user = interaction[self.USER_ID]
134 if self.restore_user_e is None or self.restore_item_e is None:
135 self.restore_user_e, self.restore_item_e = self.forward()
136 # get user embedding from storage variable
137 u_embeddings = self.restore_user_e[user]
139 # dot with all item embedding to accelerate
140 scores = torch.matmul(u_embeddings, self.restore_item_e.transpose(0, 1))
142 return scores.view(-1)