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

1# @Time : 2020/7/16 

2# @Author : Zihan Lin 

3# @Email : linzihan.super@foxmail.com 

4 

5# UPDATE: 

6# @Time : 2020/9/16 

7# @Author : Shanlei Mu 

8# @Email : slmu@ruc.edu.cn 

9 

10r"""NGCF 

11################################################ 

12Reference: 

13 Xiang Wang et al. "Neural Graph Collaborative Filtering." in SIGIR 2019. 

14 

15Reference code: 

16 https://github.com/xiangwang1223/neural_graph_collaborative_filtering 

17 

18""" 

19 

20import torch 

21import torch.nn.functional as F 

22from torch import nn 

23 

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 

29 

30 

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 """ 

35 

36 input_type = InputType.PAIRWISE 

37 

38 def __init__(self, config, dataset): 

39 super().__init__(config, dataset) 

40 

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"] 

48 

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() 

59 

60 # storage variables for full sort evaluation acceleration 

61 self.restore_user_e = None 

62 self.restore_item_e = None 

63 

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) 

67 

68 # parameters initialization 

69 self.apply(xavier_normal_initialization) 

70 self.other_parameter_name = ["restore_user_e", "restore_item_e"] 

71 

72 def get_ego_embeddings(self): 

73 r"""Get the embedding of users and items and combine to an embedding matrix. 

74 

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 

82 

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) 

94 

95 user_all_embeddings, item_all_embeddings = torch.split(ngcf_all_embeddings, [self.n_users, self.n_items]) 

96 

97 return user_all_embeddings, item_all_embeddings 

98 

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 

103 

104 user = interaction[self.USER_ID] 

105 pos_item = interaction[self.ITEM_ID] 

106 neg_item = interaction[self.NEG_ITEM_ID] 

107 

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] 

112 

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 

116 

117 reg_loss = self.reg_loss(u_embeddings, pos_embeddings, neg_embeddings) # L2 regularization of embeddings 

118 

119 return mf_loss + self.reg_weight * reg_loss 

120 

121 def predict(self, interaction): 

122 user = interaction[self.USER_ID] 

123 item = interaction[self.ITEM_ID] 

124 

125 user_all_embeddings, item_all_embeddings = self.forward() 

126 

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 

131 

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] 

138 

139 # dot with all item embedding to accelerate 

140 scores = torch.matmul(u_embeddings, self.restore_item_e.transpose(0, 1)) 

141 

142 return scores.view(-1)