Coverage for hopwise/model/general_recommender/gcmc.py: 83%

210 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-09-30 13:25 +0000

1# @Time : 2020/9/1 14:00 

2# @Author : Changxin Tian 

3# @Email : cx.tian@outlook.com 

4 

5# UPDATE 

6# @Time : 2020/10/1 

7# @Author : Changxin Tian 

8# @Email : cx.tian@outlook.com 

9 

10r"""GCMC 

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

12 

13Reference: 

14 van den Berg et al. "Graph Convolutional Matrix Completion." in SIGKDD 2018. 

15 

16Reference code: 

17 https://github.com/riannevdberg/gc-mc 

18""" 

19 

20import math 

21 

22import numpy as np 

23import torch 

24from torch import nn 

25 

26from hopwise.model.abstract_recommender import GeneralRecommender 

27from hopwise.model.layers import SparseDropout 

28from hopwise.utils import InputType 

29 

30 

31class GCMC(GeneralRecommender): 

32 r"""GCMC is a model that incorporate graph autoencoders for recommendation. 

33 

34 Graph autoencoders are comprised of: 

35 

36 1) a graph encoder model :math:`Z = f(X; A)`, which take as input an :math:`N \times D` feature matrix X and 

37 a graph adjacency matrix A, and produce an :math:`N \times E` node embedding matrix 

38 :math:`Z = [z_1^T,..., z_N^T ]^T`; 

39 

40 2) a pairwise decoder model :math:`\hat A = g(Z)`, which takes pairs of node embeddings :math:`(z_i, z_j)` and 

41 predicts respective entries :math:`\hat A_{ij}` in the adjacency matrix. 

42 

43 Note that :math:`N` denotes the number of nodes, :math:`D` the number of input features, 

44 and :math:`E` the embedding size. 

45 

46 We implement the model following the original author with a pairwise training mode. 

47 """ 

48 

49 input_type = InputType.PAIRWISE 

50 

51 def __init__(self, config, dataset): 

52 super().__init__(config, dataset) 

53 

54 # load dataset info 

55 self.num_all = self.n_users + self.n_items 

56 

57 # load parameters info 

58 self.dropout_prob = config["dropout_prob"] 

59 self.sparse_feature = config["sparse_feature"] 

60 self.gcn_output_dim = config["gcn_output_dim"] 

61 self.dense_output_dim = config["embedding_size"] 

62 self.n_class = config["class_num"] 

63 self.num_basis_functions = config["num_basis_functions"] 

64 

65 # generate node feature 

66 if self.sparse_feature: 

67 features = dataset.eye_matrix(form="torch.sparse") 

68 i = features._indices() 

69 v = features._values() 

70 self.user_features = torch.sparse.FloatTensor( 

71 i[:, : self.n_users], 

72 v[: self.n_users], 

73 torch.Size([self.n_users, self.num_all]), 

74 ).to(self.device) 

75 item_i = i[:, self.n_users :] 

76 item_i[0, :] = item_i[0, :] - self.n_users 

77 self.item_features = torch.sparse.FloatTensor( 

78 item_i, v[self.n_users :], torch.Size([self.n_items, self.num_all]) 

79 ).to(self.device) 

80 else: 

81 features = torch.eye(self.num_all).to(self.device) 

82 self.user_features, self.item_features = torch.split(features, [self.n_users, self.n_items]) 

83 self.input_dim = self.user_features.shape[1] 

84 

85 # adj matrices for each relation are stored in self.support 

86 self.Graph = dataset.norm_adjacency_matrix(form="torch.sparse").to(self.device) 

87 self.support = [self.Graph] 

88 

89 # accumulation operation 

90 self.accum = config["accum"] 

91 if self.accum == "stack": 

92 div = self.gcn_output_dim // len(self.support) 

93 if self.gcn_output_dim % len(self.support) != 0: 

94 self.logger.warning( 

95 "HIDDEN[0] (=%d) of stack layer is adjusted to %d (in %d splits)." 

96 % (self.gcn_output_dim, len(self.support) * div, len(self.support)) 

97 ) 

98 self.gcn_output_dim = len(self.support) * div 

99 

100 # define layers and loss 

101 self.GcEncoder = GcEncoder( 

102 accum=self.accum, 

103 num_user=self.n_users, 

104 num_item=self.n_items, 

105 support=self.support, 

106 input_dim=self.input_dim, 

107 gcn_output_dim=self.gcn_output_dim, 

108 dense_output_dim=self.dense_output_dim, 

109 drop_prob=self.dropout_prob, 

110 device=self.device, 

111 sparse_feature=self.sparse_feature, 

112 ).to(self.device) 

113 self.BiDecoder = BiDecoder( 

114 input_dim=self.dense_output_dim, 

115 output_dim=self.n_class, 

116 drop_prob=0.0, 

117 device=self.device, 

118 num_weights=self.num_basis_functions, 

119 ).to(self.device) 

120 self.loss_function = nn.CrossEntropyLoss() 

121 

122 def forward(self, user_X, item_X, user, item): 

123 # Graph autoencoders are comprised of a graph encoder model and a pairwise decoder model. 

124 user_embedding, item_embedding = self.GcEncoder(user_X, item_X) 

125 predict_score = self.BiDecoder(user_embedding, item_embedding, user, item) 

126 return predict_score 

127 

128 def calculate_loss(self, interaction): 

129 user = interaction[self.USER_ID] 

130 pos_item = interaction[self.ITEM_ID] 

131 neg_item = interaction[self.NEG_ITEM_ID] 

132 

133 users = torch.cat((user, user)) 

134 items = torch.cat((pos_item, neg_item)) 

135 

136 user_X, item_X = self.user_features, self.item_features 

137 predict = self.forward(user_X, item_X, users, items) 

138 target = torch.zeros(len(user) * 2, dtype=torch.long).to(self.device) 

139 target[: len(user)] = 1 

140 

141 loss = self.loss_function(predict, target) 

142 return loss 

143 

144 def predict(self, interaction): 

145 user = interaction[self.USER_ID] 

146 item = interaction[self.ITEM_ID] 

147 

148 user_X, item_X = self.user_features, self.item_features 

149 predict = self.forward(user_X, item_X, user, item) 

150 

151 score = predict[:, 1] 

152 return score 

153 

154 def full_sort_predict(self, interaction): 

155 user = interaction[self.USER_ID] 

156 

157 user_X, item_X = self.user_features, self.item_features 

158 predict = self.forward(user_X, item_X, user, None) 

159 

160 score = predict[:, 1] 

161 return score 

162 

163 

164class GcEncoder(nn.Module): 

165 r"""Graph Convolutional Encoder 

166 GcEncoder take as input an :math:`N \times D` feature matrix :math:`X` and a graph adjacency matrix :math:`A`, 

167 and produce an :math:`N \times E` node embedding matrix; 

168 Note that :math:`N` denotes the number of nodes, :math:`D` the number of input features, 

169 and :math:`E` the embedding size. 

170 """ 

171 

172 def __init__( 

173 self, 

174 accum, 

175 num_user, 

176 num_item, 

177 support, 

178 input_dim, 

179 gcn_output_dim, 

180 dense_output_dim, 

181 drop_prob, 

182 device, 

183 sparse_feature=True, 

184 act_dense=lambda x: x, 

185 share_user_item_weights=True, 

186 bias=False, 

187 ): 

188 super().__init__() 

189 self.num_users = num_user 

190 self.num_items = num_item 

191 self.input_dim = input_dim 

192 self.gcn_output_dim = gcn_output_dim 

193 self.dense_output_dim = dense_output_dim 

194 self.accum = accum 

195 self.sparse_feature = sparse_feature 

196 

197 self.device = device 

198 self.dropout_prob = drop_prob 

199 self.dropout = nn.Dropout(p=self.dropout_prob) 

200 if self.sparse_feature: 

201 self.sparse_dropout = SparseDropout(p=self.dropout_prob) 

202 else: 

203 self.sparse_dropout = nn.Dropout(p=self.dropout_prob) 

204 

205 self.dense_activate = act_dense 

206 self.activate = nn.ReLU() 

207 self.share_weights = share_user_item_weights 

208 self.bias = bias 

209 

210 self.support = support 

211 self.num_support = len(support) 

212 

213 # gcn layer 

214 if self.accum == "sum": 

215 self.weights_u = nn.ParameterList( 

216 [ 

217 nn.Parameter( 

218 torch.FloatTensor(self.input_dim, self.gcn_output_dim).to(self.device), 

219 requires_grad=True, 

220 ) 

221 for _ in range(self.num_support) 

222 ] 

223 ) 

224 if share_user_item_weights: 

225 self.weights_v = self.weights_u 

226 else: 

227 self.weights_v = nn.ParameterList( 

228 [ 

229 nn.Parameter( 

230 torch.FloatTensor(self.input_dim, self.gcn_output_dim).to(self.device), 

231 requires_grad=True, 

232 ) 

233 for _ in range(self.num_support) 

234 ] 

235 ) 

236 else: 

237 assert self.gcn_output_dim % self.num_support == 0, ( 

238 "output_dim must be multiple of num_support for stackGC" 

239 ) 

240 self.sub_hidden_dim = self.gcn_output_dim // self.num_support 

241 

242 self.weights_u = nn.ParameterList( 

243 [ 

244 nn.Parameter( 

245 torch.FloatTensor(self.input_dim, self.sub_hidden_dim).to(self.device), 

246 requires_grad=True, 

247 ) 

248 for _ in range(self.num_support) 

249 ] 

250 ) 

251 if share_user_item_weights: 

252 self.weights_v = self.weights_u 

253 else: 

254 self.weights_v = nn.ParameterList( 

255 [ 

256 nn.Parameter( 

257 torch.FloatTensor(self.input_dim, self.sub_hidden_dim).to(self.device), 

258 requires_grad=True, 

259 ) 

260 for _ in range(self.num_support) 

261 ] 

262 ) 

263 

264 # dense layer 

265 self.dense_layer_u = nn.Linear(self.gcn_output_dim, self.dense_output_dim, bias=self.bias) 

266 if share_user_item_weights: 

267 self.dense_layer_v = self.dense_layer_u 

268 else: 

269 self.dense_layer_v = nn.Linear(self.gcn_output_dim, self.dense_output_dim, bias=self.bias) 

270 

271 self._init_weights() 

272 

273 def _init_weights(self): 

274 init_range = math.sqrt((self.num_support + 1) / (self.input_dim + self.gcn_output_dim)) 

275 for w in range(self.num_support): 

276 self.weights_u[w].data.uniform_(-init_range, init_range) 

277 if not self.share_weights: 

278 for w in range(self.num_support): 

279 self.weights_v[w].data.uniform_(-init_range, init_range) 

280 

281 dense_init_range = math.sqrt((self.num_support + 1) / (self.dense_output_dim + self.gcn_output_dim)) 

282 self.dense_layer_u.weight.data.uniform_(-dense_init_range, dense_init_range) 

283 if not self.share_weights: 

284 self.dense_layer_v.weight.data.uniform_(-dense_init_range, dense_init_range) 

285 

286 if self.bias: 

287 self.dense_layer_u.bias.data.fill_(0) 

288 if not self.share_weights: 

289 self.dense_layer_v.bias.data.fill_(0) 

290 

291 def forward(self, user_X, item_X): 

292 # ----------------------------------------GCN layer---------------------------------------- 

293 

294 user_X = self.sparse_dropout(user_X) 

295 item_X = self.sparse_dropout(item_X) 

296 

297 embeddings = [] 

298 if self.accum == "sum": 

299 wu = 0.0 

300 wv = 0.0 

301 for i in range(self.num_support): 

302 # weight sharing 

303 wu = self.weights_u[i] + wu 

304 wv = self.weights_v[i] + wv 

305 

306 # multiply feature matrices with weights 

307 if self.sparse_feature: 

308 temp_u = torch.sparse.mm(user_X, wu) 

309 temp_v = torch.sparse.mm(item_X, wv) 

310 else: 

311 temp_u = torch.mm(user_X, wu) 

312 temp_v = torch.mm(item_X, wv) 

313 all_embedding = torch.cat([temp_u, temp_v]) 

314 

315 # then multiply with adj matrices 

316 graph_A = self.support[i] 

317 all_emb = torch.sparse.mm(graph_A, all_embedding) 

318 embeddings.append(all_emb) 

319 

320 embeddings = torch.stack(embeddings, dim=1) 

321 embeddings = torch.sum(embeddings, dim=1) 

322 else: 

323 for i in range(self.num_support): 

324 # multiply feature matrices with weights 

325 if self.sparse_feature: 

326 temp_u = torch.sparse.mm(user_X, self.weights_u[i]) 

327 temp_v = torch.sparse.mm(item_X, self.weights_v[i]) 

328 else: 

329 temp_u = torch.mm(user_X, self.weights_u[i]) 

330 temp_v = torch.mm(item_X, self.weights_v[i]) 

331 all_embedding = torch.cat([temp_u, temp_v]) 

332 

333 # then multiply with adj matrices 

334 graph_A = self.support[i] 

335 all_emb = torch.sparse.mm(graph_A, all_embedding) 

336 embeddings.append(all_emb) 

337 

338 embeddings = torch.cat(embeddings, dim=1) 

339 

340 users, items = torch.split(embeddings, [self.num_users, self.num_items]) 

341 

342 u_hidden = self.activate(users) 

343 v_hidden = self.activate(items) 

344 

345 # ----------------------------------------Dense Layer---------------------------------------- 

346 

347 u_hidden = self.dropout(u_hidden) 

348 v_hidden = self.dropout(v_hidden) 

349 

350 u_hidden = self.dense_layer_u(u_hidden) 

351 v_hidden = self.dense_layer_v(v_hidden) 

352 

353 u_outputs = self.dense_activate(u_hidden) 

354 v_outputs = self.dense_activate(v_hidden) 

355 

356 return u_outputs, v_outputs 

357 

358 

359class BiDecoder(nn.Module): 

360 """Bi-linear decoder 

361 BiDecoder takes pairs of node embeddings and predicts respective entries in the adjacency matrix. 

362 """ 

363 

364 def __init__(self, input_dim, output_dim, drop_prob, device, num_weights=3, act=lambda x: x): 

365 super().__init__() 

366 self.input_dim = input_dim 

367 self.output_dim = output_dim 

368 self.num_weights = num_weights 

369 self.device = device 

370 

371 self.activate = act 

372 self.dropout_prob = drop_prob 

373 self.dropout = nn.Dropout(p=self.dropout_prob) 

374 

375 self.weights = nn.ParameterList( 

376 [ 

377 nn.Parameter(orthogonal([self.input_dim, self.input_dim]).to(self.device)) 

378 for _ in range(self.num_weights) 

379 ] 

380 ) 

381 self.dense_layer = nn.Linear(self.num_weights, self.output_dim, bias=False) 

382 self._init_weights() 

383 

384 def _init_weights(self): 

385 dense_init_range = math.sqrt(self.output_dim / (self.num_weights + self.output_dim)) 

386 self.dense_layer.weight.data.uniform_(-dense_init_range, dense_init_range) 

387 

388 def forward(self, u_inputs, i_inputs, users, items=None): 

389 u_inputs = self.dropout(u_inputs) 

390 i_inputs = self.dropout(i_inputs) 

391 

392 if items is not None: 

393 users_emb = u_inputs[users] 

394 items_emb = i_inputs[items] 

395 

396 basis_outputs = [] 

397 for i in range(self.num_weights): 

398 users_emb_temp = torch.mm(users_emb, self.weights[i]) 

399 scores = torch.mul(users_emb_temp, items_emb) 

400 scores = torch.sum(scores, dim=1) 

401 basis_outputs.append(scores) 

402 else: 

403 users_emb = u_inputs[users] 

404 items_emb = i_inputs 

405 

406 basis_outputs = [] 

407 for i in range(self.num_weights): 

408 users_emb_temp = torch.mm(users_emb, self.weights[i]) 

409 scores = torch.mm(users_emb_temp, items_emb.transpose(0, 1)) 

410 basis_outputs.append(scores.view(-1)) 

411 

412 basis_outputs = torch.stack(basis_outputs, dim=1) 

413 basis_outputs = self.dense_layer(basis_outputs) 

414 output = self.activate(basis_outputs) 

415 

416 return output 

417 

418 

419def orthogonal(shape, scale=1.1): 

420 """Initialization function for weights in class GCMC. 

421 From Lasagne. Reference: Saxe et al., http://arxiv.org/abs/1312.6120 

422 """ 

423 flat_shape = (shape[0], np.prod(shape[1:])) 

424 a = np.random.normal(0.0, 1.0, flat_shape) 

425 u, _, v = np.linalg.svd(a, full_matrices=False) 

426 

427 # pick the one with the correct shape 

428 q = u if u.shape == flat_shape else v 

429 q = q.reshape(shape) 

430 return torch.tensor(scale * q[: shape[0], : shape[1]], dtype=torch.float32)