Coverage for hopwise/model/general_recommender/nncf.py: 53%
192 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 : 2021/1/14
2# @Author : Chengyuan Li
3# @Email : 2017202049@ruc.edu.cn
5r"""NNCF
6################################################
7Reference:
8 Ting Bai et al. "A Neural Collaborative Filtering Model with Interaction-based Neighborhood." in CIKM 2017.
10Reference code:
11 https://github.com/Tbbaby/NNCF-Pytorch
13"""
15import numpy as np
16import torch
17from torch import nn
18from torch.nn.init import normal_
20from hopwise.model.abstract_recommender import GeneralRecommender
21from hopwise.model.general_recommender.itemknn import ComputeSimilarity
22from hopwise.model.layers import MLPLayers
23from hopwise.utils import InputType
26class NNCF(GeneralRecommender):
27 r"""NNCF is an neural network enhanced matrix factorization model which also captures neighborhood information.
28 We implement the NNCF model with three ways to process neighborhood information.
29 """
31 input_type = InputType.POINTWISE
33 def __init__(self, config, dataset):
34 super().__init__(config, dataset)
36 # load dataset info
37 self.LABEL = config["LABEL_FIELD"]
38 self.interaction_matrix = dataset.inter_matrix(form="coo").astype(np.float32)
40 # load parameters info
41 self.ui_embedding_size = config["ui_embedding_size"]
42 self.neigh_embedding_size = config["neigh_embedding_size"]
43 self.num_conv_kernel = config["num_conv_kernel"]
44 self.conv_kernel_size = config["conv_kernel_size"]
45 self.pool_kernel_size = config["pool_kernel_size"]
46 self.mlp_hidden_size = config["mlp_hidden_size"]
47 self.neigh_num = config["neigh_num"]
48 self.neigh_info_method = config["neigh_info_method"]
49 self.resolution = config["resolution"]
51 # define layers and loss
52 self.user_embedding = nn.Embedding(self.n_users, self.ui_embedding_size)
53 self.item_embedding = nn.Embedding(self.n_items, self.ui_embedding_size)
54 self.user_neigh_embedding = nn.Embedding(self.n_items, self.neigh_embedding_size)
55 self.item_neigh_embedding = nn.Embedding(self.n_users, self.neigh_embedding_size)
56 self.user_conv = nn.Sequential(
57 nn.Conv1d(self.neigh_embedding_size, self.num_conv_kernel, self.conv_kernel_size),
58 nn.MaxPool1d(self.pool_kernel_size),
59 nn.ReLU(),
60 )
61 self.item_conv = nn.Sequential(
62 nn.Conv1d(self.neigh_embedding_size, self.num_conv_kernel, self.conv_kernel_size),
63 nn.MaxPool1d(self.pool_kernel_size),
64 nn.ReLU(),
65 )
66 conved_size = self.neigh_num - (self.conv_kernel_size - 1)
67 pooled_size = (conved_size - (self.pool_kernel_size - 1) - 1) // self.pool_kernel_size + 1
68 self.mlp_layers = MLPLayers(
69 [2 * pooled_size * self.num_conv_kernel + self.ui_embedding_size] + self.mlp_hidden_size,
70 config["dropout"],
71 )
72 self.out_layer = nn.Linear(self.mlp_hidden_size[-1], 1)
73 self.dropout_layer = torch.nn.Dropout(p=config["dropout"])
74 self.loss = nn.BCEWithLogitsLoss()
76 # choose the method to use neighborhood information
77 if self.neigh_info_method == "random":
78 self.u_neigh, self.i_neigh = self.get_neigh_random()
79 elif self.neigh_info_method == "knn":
80 self.u_neigh, self.i_neigh = self.get_neigh_knn()
81 elif self.neigh_info_method == "louvain":
82 self.u_neigh, self.i_neigh = self.get_neigh_louvain()
83 else:
84 raise RuntimeError(
85 "You need to choose the right algorithm of processing neighborhood information. \
86 The parameter neigh_info_method can be set to random, knn or louvain."
87 )
89 # parameters initialization
90 self.apply(self._init_weights)
92 def _init_weights(self, module):
93 if isinstance(module, nn.Embedding):
94 normal_(module.weight.data, mean=0.0, std=0.01)
96 # Unify embedding length
97 def Max_ner(self, lst, max_ner):
98 r"""Unify embedding length of neighborhood information for efficiency consideration.
99 Truncate the list if the length is larger than max_ner.
100 Otherwise, pad it with 0.
102 Args:
103 lst (list): The input list contains node's neighbors.
104 max_ner (int): The number of neighbors we choose for each node.
106 Returns:
107 list: The list of a node's community neighbors.
110 """
111 for i in range(len(lst)):
112 if len(lst[i]) >= max_ner:
113 lst[i] = lst[i][:max_ner]
114 else:
115 length = len(lst[i])
116 for _ in range(max_ner - length):
117 lst[i].append(0)
118 return lst
120 # Find other nodes in the same community
121 def get_community_member(self, partition, community_dict, node, kind):
122 r"""Find other nodes in the same community.
123 e.g. If the node starts with letter "i",
124 the other nodes start with letter "i" in the same community dict group are its community neighbors.
126 Args:
127 partition (dict): The input dict that contains the community each node belongs.
128 community_dict (dict): The input dict that shows the nodes each community contains.
129 node (int): The id of the input node.
130 kind (char): The type of the input node.
132 Returns:
133 list: The list of a node's community neighbors.
135 """
136 comm = community_dict[partition[node]]
137 return [x for x in comm if x.startswith(kind)]
139 # Prepare neiborhood embeddings, i.e. I(u) and U(i)
140 def prepare_vector_element(self, partition, relation, community_dict):
141 r"""Find the community neighbors of each node, i.e. I(u) and U(i).
142 Then reset the id of nodes.
144 Args:
145 partition (dict): The input dict that contains the community each node belongs.
146 relation (list): The input list that contains the relationships of users and items.
147 community_dict (dict): The input dict that shows the nodes each community contains.
149 Returns:
150 list: The list of nodes' community neighbors.
152 """
153 item2user_neighbor_lst = [[] for _ in range(self.n_items)]
154 user2item_neighbor_lst = [[] for _ in range(self.n_users)]
156 for r in range(len(relation)):
157 user, item = relation[r][0], relation[r][1]
158 item2user_neighbor = self.get_community_member(partition, community_dict, user, "u")
159 np.random.shuffle(item2user_neighbor)
160 user2item_neighbor = self.get_community_member(partition, community_dict, item, "i")
161 np.random.shuffle(user2item_neighbor)
162 _, user = user.split("_", 1)
163 user = int(user)
164 _, item = item.split("_", 1)
165 item = int(item)
166 for i in range(len(item2user_neighbor)):
167 name, index = item2user_neighbor[i].split("_", 1)
168 item2user_neighbor[i] = int(index)
169 for i in range(len(user2item_neighbor)):
170 name, index = user2item_neighbor[i].split("_", 1)
171 user2item_neighbor[i] = int(index)
173 item2user_neighbor_lst[item] = item2user_neighbor
174 user2item_neighbor_lst[user] = user2item_neighbor
176 return user2item_neighbor_lst, item2user_neighbor_lst
178 # Get neighborhood embeddings using louvain method
179 def get_neigh_louvain(self):
180 r"""Get neighborhood information using louvain algorithm.
181 First, change the id of node,
182 for example, the id of user node "1" will be set to "u_1" in order to use louvain algorithm.
183 Second, use louvain algorithm to seperate nodes into different communities.
184 Finally, find the community neighbors of each node with the same type and reset the id of the nodes.
186 Returns:
187 torch.IntTensor: The neighborhood nodes of a batch of user or item, shape: [batch_size, neigh_num]
188 """
189 inter_M = self.interaction_matrix
190 pairs = list(zip(inter_M.row, inter_M.col))
192 tmp_relation = []
193 for i in range(len(pairs)):
194 tmp_relation.append(["user_" + str(pairs[i][0]), "item_" + str(pairs[i][1])])
196 import networkx as nx
198 G = nx.Graph()
199 G.add_edges_from(tmp_relation)
200 resolution = self.resolution
201 import community
203 partition = community.best_partition(G, resolution=resolution)
205 community_dict = {}
206 community_dict.setdefault(0, [])
207 for i in range(len(partition.values())):
208 community_dict[i] = []
209 for node, part in partition.items():
210 community_dict[part] = community_dict[part] + [node]
212 tmp_user2item, tmp_item2user = self.prepare_vector_element(partition, tmp_relation, community_dict)
213 u_neigh = self.Max_ner(tmp_user2item, self.neigh_num)
214 i_neigh = self.Max_ner(tmp_item2user, self.neigh_num)
216 u_neigh = torch.tensor(u_neigh, device=self.device)
217 i_neigh = torch.tensor(i_neigh, device=self.device)
218 return u_neigh, i_neigh
220 # Get neighborhood embeddings using knn method
221 def get_neigh_knn(self):
222 r"""Get neighborhood information using knn algorithm.
223 Find direct neighbors of each node, if the number of direct neighbors is less than neigh_num,
224 add other similar neighbors using knn algorithm.
225 Otherwise, select random top k direct neighbors, k equals to the number of neighbors.
227 Returns:
228 torch.IntTensor: The neighborhood nodes of a batch of user or item, shape: [batch_size, neigh_num]
229 """
230 inter_M = self.interaction_matrix
231 pairs = list(zip(inter_M.row, inter_M.col))
232 ui_inters = np.zeros((self.n_users, self.n_items), dtype=np.int8)
234 for i in range(len(pairs)):
235 ui_inters[pairs[i][0], pairs[i][1]] = 1
237 # Get similar neighbors using knn algorithm
238 user_knn, _ = ComputeSimilarity(self.interaction_matrix.tocsr(), topk=self.neigh_num).compute_similarity(
239 "user"
240 )
241 item_knn, _ = ComputeSimilarity(self.interaction_matrix.tocsr(), topk=self.neigh_num).compute_similarity(
242 "item"
243 )
245 u_neigh, i_neigh = [], []
247 for u in range(self.n_users):
248 neigh_list = ui_inters[u].nonzero()[0]
249 direct_neigh_num = len(neigh_list)
250 if len(neigh_list) == 0:
251 u_neigh.append(self.neigh_num * [0])
252 elif direct_neigh_num < self.neigh_num:
253 # tmp_k = self.neigh_num - direct_neigh_num
254 mask = np.random.randint(0, len(neigh_list), size=1)
255 neigh_list = list(neigh_list) + list(item_knn[neigh_list[mask[0]]])
256 u_neigh.append(neigh_list[: self.neigh_num])
257 else:
258 mask = np.random.randint(0, len(neigh_list), size=self.neigh_num)
259 u_neigh.append(neigh_list[mask])
261 for i in range(self.n_items):
262 neigh_list = ui_inters[:, i].nonzero()[0]
263 direct_neigh_num = len(neigh_list)
264 if len(neigh_list) == 0:
265 i_neigh.append(self.neigh_num * [0])
266 elif direct_neigh_num < self.neigh_num:
267 mask = np.random.randint(0, len(neigh_list), size=1)
268 neigh_list = list(neigh_list) + list(user_knn[neigh_list[mask[0]]])
269 i_neigh.append(neigh_list[: self.neigh_num])
270 else:
271 mask = np.random.randint(0, len(neigh_list), size=self.neigh_num)
272 i_neigh.append(neigh_list[mask])
274 u_neigh = torch.tensor(u_neigh, device=self.device)
275 i_neigh = torch.tensor(i_neigh, device=self.device)
276 return u_neigh, i_neigh
278 # Get neighborhood embeddings using random method
279 def get_neigh_random(self):
280 r"""Get neighborhood information using random algorithm.
281 Select random top k direct neighbors, k equals to the number of neighbors.
283 Returns:
284 torch.IntTensor: The neighborhood nodes of a batch of user or item, shape: [batch_size, neigh_num]
285 """
286 inter_M = self.interaction_matrix
287 pairs = list(zip(inter_M.row, inter_M.col))
288 ui_inters = np.zeros((self.n_users, self.n_items), dtype=np.int8)
290 for i in range(len(pairs)):
291 ui_inters[pairs[i][0], pairs[i][1]] = 1
293 u_neigh, i_neigh = [], []
295 for u in range(self.n_users):
296 neigh_list = ui_inters[u].nonzero()[0]
297 if len(neigh_list) == 0:
298 u_neigh.append(self.neigh_num * [0])
299 else:
300 mask = np.random.randint(0, len(neigh_list), size=self.neigh_num)
301 u_neigh.append(neigh_list[mask])
303 for i in range(self.n_items):
304 neigh_list = ui_inters[:, i].nonzero()[0]
305 if len(neigh_list) == 0:
306 i_neigh.append(self.neigh_num * [0])
307 else:
308 mask = np.random.randint(0, len(neigh_list), size=self.neigh_num)
309 i_neigh.append(neigh_list[mask])
311 u_neigh = torch.tensor(np.array(u_neigh), device=self.device)
312 i_neigh = torch.tensor(np.array(i_neigh), device=self.device)
313 return u_neigh, i_neigh
315 # Get neighborhood embeddings
316 def get_neigh_info(self, user, item):
317 r"""Get a batch of neighborhood embedding tensor according to input id.
319 Args:
320 user (torch.LongTensor): The input tensor that contains user's id, shape: [batch_size, ]
321 item (torch.LongTensor): The input tensor that contains item's id, shape: [batch_size, ]
323 Returns:
324 torch.FloatTensor: The neighborhood embedding tensor of a batch of user, shape: [batch_size, neigh_embedding_size]
325 torch.FloatTensor: The neighborhood embedding tensor of a batch of item, shape: [batch_size, neigh_embedding_size]
327 """ # noqa: E501
328 batch_u_neigh = self.u_neigh[user]
329 batch_i_neigh = self.i_neigh[item]
330 return batch_u_neigh, batch_i_neigh
332 def forward(self, user, item):
333 user_embedding = self.user_embedding(user)
334 item_embedding = self.item_embedding(item)
336 user_neigh_input, item_neigh_input = self.get_neigh_info(user, item)
337 user_neigh_embedding = self.user_neigh_embedding(user_neigh_input)
338 item_neigh_embedding = self.item_neigh_embedding(item_neigh_input)
339 user_neigh_embedding = user_neigh_embedding.permute(0, 2, 1)
340 user_neigh_conv_embedding = self.user_conv(user_neigh_embedding)
341 # batch_size * out_channel * pool_size
342 batch_size = user_neigh_conv_embedding.size(0)
343 user_neigh_conv_embedding = user_neigh_conv_embedding.view(batch_size, -1)
344 item_neigh_embedding = item_neigh_embedding.permute(0, 2, 1)
345 item_neigh_conv_embedding = self.item_conv(item_neigh_embedding)
346 # batch_size * out_channel * pool_size
347 item_neigh_conv_embedding = item_neigh_conv_embedding.view(batch_size, -1)
348 mf_vec = torch.mul(user_embedding, item_embedding)
349 last = torch.cat((mf_vec, user_neigh_conv_embedding, item_neigh_conv_embedding), dim=-1)
351 output = self.mlp_layers(last)
352 out = self.out_layer(output)
353 out = out.squeeze(-1)
354 return out
356 def calculate_loss(self, interaction):
357 user = interaction[self.USER_ID]
358 item = interaction[self.ITEM_ID]
359 label = interaction[self.LABEL]
361 output = self.forward(user, item)
362 return self.loss(output, label)
364 def predict(self, interaction):
365 user = interaction[self.USER_ID]
366 item = interaction[self.ITEM_ID]
367 return torch.sigmoid(self.forward(user, item))