├── Data ├── README ├── amazon-book │ ├── README.md │ ├── item_list.txt │ ├── test.txt │ ├── train.txt │ └── user_list.txt └── gowalla │ ├── README.md │ ├── item_list.txt │ ├── test.txt │ ├── train.txt │ └── user_list.txt ├── LICENSE ├── NGCF ├── BPRMF.py ├── NGCF.py ├── NMF.py ├── README.md └── utility │ ├── README.md │ ├── batch_test.py │ ├── helper.py │ ├── load_data.py │ ├── metrics.py │ └── parser.py └── README.md /Data/README: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Data/amazon-book/README.md: -------------------------------------------------------------------------------- 1 | Look for the full dataset? Please visit the [websit](http://jmcauley.ucsd.edu/data/amazon). 2 | -------------------------------------------------------------------------------- /Data/gowalla/README.md: -------------------------------------------------------------------------------- 1 | Look for the full dataset? 2 | Please visit the [websit](https://snap.stanford.edu/data/loc-gowalla.html). 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Xiang Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NGCF/BPRMF.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Oct 10, 2018 3 | Tensorflow Implementation of the baseline of "Matrix Factorization with BPR Loss" in: 4 | Wang Xiang et al. Neural Graph Collaborative Filtering. In SIGIR 2019. 5 | @author: Xiang Wang (xiangwang@u.nus.edu) 6 | ''' 7 | import tensorflow as tf 8 | from utility.helper import * 9 | import numpy as np 10 | from scipy.sparse import csr_matrix 11 | from utility.batch_test import * 12 | import os 13 | import sys 14 | os.environ['TF_CPP_MIN_LOG_LEVEL']='2' 15 | 16 | class BPRMF(object): 17 | def __init__(self, data_config): 18 | self.model_type = 'bprmf' 19 | 20 | self.n_users = data_config['n_users'] 21 | self.n_items = data_config['n_items'] 22 | 23 | self.lr = args.lr 24 | # self.lr_decay = args.lr_decay 25 | 26 | self.emb_dim = args.embed_size 27 | self.batch_size = args.batch_size 28 | 29 | self.weight_size = eval(args.layer_size) 30 | self.n_layers = len(self.weight_size) 31 | 32 | self.regs = eval(args.regs) 33 | self.decay = self.regs[0] 34 | 35 | self.verbose = args.verbose 36 | 37 | # placeholder definition 38 | self.users = tf.placeholder(tf.int32, shape=(None,)) 39 | self.pos_items = tf.placeholder(tf.int32, shape=(None,)) 40 | self.neg_items = tf.placeholder(tf.int32, shape=(None,)) 41 | 42 | # self.global_step = tf.Variable(0, trainable=False) 43 | 44 | self.weights = self._init_weights() 45 | 46 | # Original embedding. 47 | u_e = tf.nn.embedding_lookup(self.weights['user_embedding'], self.users) 48 | pos_i_e = tf.nn.embedding_lookup(self.weights['item_embedding'], self.pos_items) 49 | neg_i_e = tf.nn.embedding_lookup(self.weights['item_embedding'], self.neg_items) 50 | 51 | # All ratings for all users. 52 | self.batch_ratings = tf.matmul(u_e, pos_i_e, transpose_a=False, transpose_b=True) 53 | 54 | self.mf_loss, self.reg_loss = self.create_bpr_loss(u_e, pos_i_e, neg_i_e) 55 | self.loss = self.mf_loss + self.reg_loss 56 | 57 | # self.dy_lr = tf.train.exponential_decay(self.lr, self.global_step, 10000, self.lr_decay, staircase=True) 58 | self.opt = tf.train.RMSPropOptimizer(learning_rate=self.lr).minimize(self.loss) 59 | 60 | # self.updates = self.opt.minimize(self.loss, var_list=self.weights) 61 | 62 | self._statistics_params() 63 | 64 | def _init_weights(self): 65 | all_weights = dict() 66 | 67 | initializer = tf.contrib.layers.xavier_initializer() 68 | 69 | 70 | all_weights['user_embedding'] = tf.Variable(initializer([self.n_users, self.emb_dim]), name='user_embedding') 71 | all_weights['item_embedding'] = tf.Variable(initializer([self.n_items, self.emb_dim]), name='item_embedding') 72 | 73 | return all_weights 74 | 75 | def create_bpr_loss(self, users, pos_items, neg_items): 76 | pos_scores = tf.reduce_sum(tf.multiply(users, pos_items), axis=1) 77 | neg_scores = tf.reduce_sum(tf.multiply(users, neg_items), axis=1) 78 | 79 | regularizer = tf.nn.l2_loss(users) + tf.nn.l2_loss(pos_items) + tf.nn.l2_loss(neg_items) 80 | regularizer = regularizer/self.batch_size 81 | 82 | maxi = tf.log(tf.nn.sigmoid(pos_scores - neg_scores)) 83 | 84 | mf_loss = tf.negative(tf.reduce_mean(maxi)) 85 | reg_loss = self.decay * regularizer 86 | return mf_loss, reg_loss 87 | 88 | 89 | def _statistics_params(self): 90 | # number of params 91 | total_parameters = 0 92 | for variable in self.weights.values(): 93 | shape = variable.get_shape() # shape is an array of tf.Dimension 94 | variable_parameters = 1 95 | for dim in shape: 96 | variable_parameters *= dim.value 97 | total_parameters += variable_parameters 98 | if self.verbose > 0: 99 | print("#params: %d" % total_parameters) 100 | 101 | if __name__ == '__main__': 102 | os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu_id) 103 | 104 | config = dict() 105 | config['n_users'] = data_generator.n_users 106 | config['n_items'] = data_generator.n_items 107 | 108 | t0 = time() 109 | 110 | model = BPRMF(data_config=config) 111 | 112 | saver = tf.train.Saver() 113 | 114 | # ********************************************************* 115 | # save the model parameters. 116 | if args.save_flag == 1: 117 | weights_save_path = '%sweights/%s/%s/l%s_r%s' % (args.proj_path, args.dataset, model.model_type, str(args.lr), 118 | '-'.join([str(r) for r in eval(args.regs)])) 119 | ensureDir(weights_save_path) 120 | save_saver = tf.train.Saver(max_to_keep=1) 121 | 122 | config = tf.ConfigProto() 123 | config.gpu_options.allow_growth = True 124 | sess = tf.Session(config=config) 125 | 126 | # ********************************************************* 127 | # reload the pretrained model parameters. 128 | if args.pretrain == 1: 129 | pretrain_path = '%sweights/%s/%s/l%s_r%s' % (args.proj_path, args.dataset, model.model_type, str(args.lr), 130 | '-'.join([str(r) for r in eval(args.regs)])) 131 | ckpt = tf.train.get_checkpoint_state(os.path.dirname(pretrain_path + '/checkpoint')) 132 | if ckpt and ckpt.model_checkpoint_path: 133 | sess.run(tf.global_variables_initializer()) 134 | saver.restore(sess, ckpt.model_checkpoint_path) 135 | print('load the pretrained model parameters from: ', pretrain_path) 136 | 137 | # ********************************************************* 138 | # get the performance from pretrained model. 139 | users_to_test = list(data_generator.test_set.keys()) 140 | ret = test(sess, model, users_to_test, drop_flag=False) 141 | cur_best_pre_0 = ret['recall'][0] 142 | 143 | pretrain_ret = 'pretrained model recall=[%.5f, %.5f], precision=[%.5f, %.5f], hit=[%.5f, %.5f],' \ 144 | 'ndcg=[%.5f, %.5f]' % \ 145 | (ret['recall'][0], ret['recall'][-1], 146 | ret['precision'][0], ret['precision'][-1], 147 | ret['hit_ratio'][0], ret['hit_ratio'][-1], 148 | ret['ndcg'][0], ret['ndcg'][-1]) 149 | print(pretrain_ret) 150 | else: 151 | sess.run(tf.global_variables_initializer()) 152 | cur_best_pre_0 = 0. 153 | print('without pretraining.') 154 | else: 155 | sess.run(tf.global_variables_initializer()) 156 | cur_best_pre_0 = 0. 157 | print('without pretraining.') 158 | 159 | 160 | loss_loger, pre_loger, rec_loger, ndcg_loger, hit_loger = [], [], [], [], [] 161 | stopping_step = 0 162 | 163 | for epoch in range(args.epoch): 164 | t1 = time() 165 | loss, mf_loss, reg_loss = 0., 0., 0. 166 | n_batch = data_generator.n_train // args.batch_size + 1 167 | 168 | 169 | for idx in range(n_batch): 170 | # btime= time() 171 | users, pos_items, neg_items = data_generator.sample() 172 | _, batch_loss, batch_mf_loss, batch_reg_loss = sess.run([model.opt, model.loss, model.mf_loss, model.reg_loss], 173 | feed_dict={model.users: users, model.pos_items: pos_items, 174 | model.neg_items: neg_items}) 175 | loss += batch_loss 176 | mf_loss += batch_mf_loss 177 | reg_loss += batch_reg_loss 178 | # print(time() - btime) 179 | 180 | if np.isnan(loss) == True: 181 | print('ERROR: loss is nan.') 182 | sys.exit() 183 | 184 | # print the test evaluation metrics each 10 epochs; pos:neg = 1:10. 185 | if (epoch + 1) % 10 != 0: 186 | if args.verbose > 0 and epoch % args.verbose == 0: 187 | perf_str = 'Epoch %d [%.1fs]: train==[%.5f=%.5f + %.5f]' % (epoch, time()-t1, loss, mf_loss, reg_loss) 188 | print(perf_str) 189 | continue 190 | 191 | t2 = time() 192 | users_to_test = list(data_generator.test_set.keys()) 193 | ret = test(sess, model, users_to_test, drop_flag=False) 194 | 195 | t3 = time() 196 | 197 | loss_loger.append(loss) 198 | rec_loger.append(ret['recall']) 199 | pre_loger.append(ret['precision']) 200 | ndcg_loger.append(ret['ndcg']) 201 | hit_loger.append(ret['hit_ratio']) 202 | 203 | if args.verbose > 0: 204 | perf_str = 'Epoch %d [%.1fs + %.1fs]: train==[%.5f=%.5f + %.5f], recall=[%.5f, %.5f], ' \ 205 | 'precision=[%.5f, %.5f], hit=[%.5f, %.5f], ndcg=[%.5f, %.5f]' % \ 206 | (epoch, t2 - t1, t3 - t2, loss, mf_loss, reg_loss, ret['recall'][0], ret['recall'][-1], 207 | ret['precision'][0], ret['precision'][-1], ret['hit_ratio'][0], ret['hit_ratio'][-1], 208 | ret['ndcg'][0], ret['ndcg'][-1]) 209 | print(perf_str) 210 | 211 | cur_best_pre_0, stopping_step, should_stop = early_stopping(ret['recall'][0], cur_best_pre_0, 212 | stopping_step, expected_order='acc', flag_step=10) 213 | if should_stop == True: 214 | break 215 | # ********************************************************* 216 | # save the user & item embeddings for pretraining. 217 | if ret['recall'][0] == cur_best_pre_0 and args.save_flag == 1: 218 | save_saver.save(sess, weights_save_path + '/weights', global_step=epoch) 219 | print('save the weights in path: ', weights_save_path) 220 | 221 | 222 | recs = np.array(rec_loger) 223 | pres = np.array(pre_loger) 224 | ndcgs = np.array(ndcg_loger) 225 | hit = np.array(hit_loger) 226 | 227 | best_rec_0 = max(pres[:, 0]) 228 | idx = list(pres[:, 0]).index(best_rec_0) 229 | 230 | final_perf = "Best Iter=[%d]@[%.1f]\trecall=[%s], precision=[%s], hit=[%s], ndcg=[%s]" % \ 231 | (idx, time() - t0, '\t'.join(['%.5f' % r for r in recs[idx]]), 232 | '\t'.join(['%.5f' % r for r in pres[idx]]), 233 | '\t'.join(['%.5f' % r for r in hit[idx]]), 234 | '\t'.join(['%.5f' % r for r in ndcgs[idx]])) 235 | print(final_perf) 236 | 237 | save_path = '%soutput_final/%s/%s.result' % (args.proj_path, args.dataset, model.model_type) 238 | ensureDir(save_path) 239 | f = open(save_path, 'a') 240 | 241 | f.write('embed_size=%d, lr=%.4f, regs=%s, \n\t%s\n' % (args.embed_size, args.lr, args.regs, 242 | final_perf)) 243 | f.close() 244 | -------------------------------------------------------------------------------- /NGCF/NGCF.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Oct 10, 2018 3 | Tensorflow Implementation of Neural Graph Collaborative Filtering (NGCF) model in: 4 | Wang Xiang et al. Neural Graph Collaborative Filtering. In SIGIR 2019. 5 | 6 | @author: Xiang Wang (xiangwang@u.nus.edu) 7 | ''' 8 | import tensorflow as tf 9 | import os 10 | import sys 11 | os.environ['TF_CPP_MIN_LOG_LEVEL']='2' 12 | 13 | from utility.helper import * 14 | from utility.batch_test import * 15 | 16 | class NGCF(object): 17 | def __init__(self, data_config, pretrain_data): 18 | # argument settings 19 | self.model_type = 'ngcf' 20 | self.adj_type = args.adj_type 21 | self.alg_type = args.alg_type 22 | 23 | self.pretrain_data = pretrain_data 24 | 25 | self.n_users = data_config['n_users'] 26 | self.n_items = data_config['n_items'] 27 | 28 | self.n_fold = 100 29 | 30 | self.norm_adj = data_config['norm_adj'] 31 | self.n_nonzero_elems = self.norm_adj.count_nonzero() 32 | 33 | self.lr = args.lr 34 | 35 | self.emb_dim = args.embed_size 36 | self.batch_size = args.batch_size 37 | 38 | self.weight_size = eval(args.layer_size) 39 | self.n_layers = len(self.weight_size) 40 | 41 | self.model_type += '_%s_%s_l%d' % (self.adj_type, self.alg_type, self.n_layers) 42 | 43 | self.regs = eval(args.regs) 44 | self.decay = self.regs[0] 45 | 46 | self.verbose = args.verbose 47 | 48 | ''' 49 | ********************************************************* 50 | Create Placeholder for Input Data & Dropout. 51 | ''' 52 | # placeholder definition 53 | self.users = tf.placeholder(tf.int32, shape=(None,)) 54 | self.pos_items = tf.placeholder(tf.int32, shape=(None,)) 55 | self.neg_items = tf.placeholder(tf.int32, shape=(None,)) 56 | 57 | # dropout: node dropout (adopted on the ego-networks); 58 | # ... since the usage of node dropout have higher computational cost, 59 | # ... please use the 'node_dropout_flag' to indicate whether use such technique. 60 | # message dropout (adopted on the convolution operations). 61 | self.node_dropout_flag = args.node_dropout_flag 62 | self.node_dropout = tf.placeholder(tf.float32, shape=[None]) 63 | self.mess_dropout = tf.placeholder(tf.float32, shape=[None]) 64 | 65 | """ 66 | ********************************************************* 67 | Create Model Parameters (i.e., Initialize Weights). 68 | """ 69 | # initialization of model parameters 70 | self.weights = self._init_weights() 71 | 72 | """ 73 | ********************************************************* 74 | Compute Graph-based Representations of all users & items via Message-Passing Mechanism of Graph Neural Networks. 75 | Different Convolutional Layers: 76 | 1. ngcf: defined in 'Neural Graph Collaborative Filtering', SIGIR2019; 77 | 2. gcn: defined in 'Semi-Supervised Classification with Graph Convolutional Networks', ICLR2018; 78 | 3. gcmc: defined in 'Graph Convolutional Matrix Completion', KDD2018; 79 | """ 80 | if self.alg_type in ['ngcf']: 81 | self.ua_embeddings, self.ia_embeddings = self._create_ngcf_embed() 82 | 83 | elif self.alg_type in ['gcn']: 84 | self.ua_embeddings, self.ia_embeddings = self._create_gcn_embed() 85 | 86 | elif self.alg_type in ['gcmc']: 87 | self.ua_embeddings, self.ia_embeddings = self._create_gcmc_embed() 88 | 89 | """ 90 | ********************************************************* 91 | Establish the final representations for user-item pairs in batch. 92 | """ 93 | self.u_g_embeddings = tf.nn.embedding_lookup(self.ua_embeddings, self.users) 94 | self.pos_i_g_embeddings = tf.nn.embedding_lookup(self.ia_embeddings, self.pos_items) 95 | self.neg_i_g_embeddings = tf.nn.embedding_lookup(self.ia_embeddings, self.neg_items) 96 | 97 | """ 98 | ********************************************************* 99 | Inference for the testing phase. 100 | """ 101 | self.batch_ratings = tf.matmul(self.u_g_embeddings, self.pos_i_g_embeddings, transpose_a=False, transpose_b=True) 102 | 103 | """ 104 | ********************************************************* 105 | Generate Predictions & Optimize via BPR loss. 106 | """ 107 | self.mf_loss, self.emb_loss, self.reg_loss = self.create_bpr_loss(self.u_g_embeddings, 108 | self.pos_i_g_embeddings, 109 | self.neg_i_g_embeddings) 110 | self.loss = self.mf_loss + self.emb_loss + self.reg_loss 111 | 112 | self.opt = tf.train.AdamOptimizer(learning_rate=self.lr).minimize(self.loss) 113 | 114 | def _init_weights(self): 115 | all_weights = dict() 116 | 117 | initializer = tf.contrib.layers.xavier_initializer() 118 | 119 | if self.pretrain_data is None: 120 | all_weights['user_embedding'] = tf.Variable(initializer([self.n_users, self.emb_dim]), name='user_embedding') 121 | all_weights['item_embedding'] = tf.Variable(initializer([self.n_items, self.emb_dim]), name='item_embedding') 122 | print('using xavier initialization') 123 | else: 124 | all_weights['user_embedding'] = tf.Variable(initial_value=self.pretrain_data['user_embed'], trainable=True, 125 | name='user_embedding', dtype=tf.float32) 126 | all_weights['item_embedding'] = tf.Variable(initial_value=self.pretrain_data['item_embed'], trainable=True, 127 | name='item_embedding', dtype=tf.float32) 128 | print('using pretrained initialization') 129 | 130 | self.weight_size_list = [self.emb_dim] + self.weight_size 131 | 132 | for k in range(self.n_layers): 133 | all_weights['W_gc_%d' %k] = tf.Variable( 134 | initializer([self.weight_size_list[k], self.weight_size_list[k+1]]), name='W_gc_%d' % k) 135 | all_weights['b_gc_%d' %k] = tf.Variable( 136 | initializer([1, self.weight_size_list[k+1]]), name='b_gc_%d' % k) 137 | 138 | all_weights['W_bi_%d' % k] = tf.Variable( 139 | initializer([self.weight_size_list[k], self.weight_size_list[k + 1]]), name='W_bi_%d' % k) 140 | all_weights['b_bi_%d' % k] = tf.Variable( 141 | initializer([1, self.weight_size_list[k + 1]]), name='b_bi_%d' % k) 142 | 143 | all_weights['W_mlp_%d' % k] = tf.Variable( 144 | initializer([self.weight_size_list[k], self.weight_size_list[k+1]]), name='W_mlp_%d' % k) 145 | all_weights['b_mlp_%d' % k] = tf.Variable( 146 | initializer([1, self.weight_size_list[k+1]]), name='b_mlp_%d' % k) 147 | 148 | return all_weights 149 | 150 | def _split_A_hat(self, X): 151 | A_fold_hat = [] 152 | 153 | fold_len = (self.n_users + self.n_items) // self.n_fold 154 | for i_fold in range(self.n_fold): 155 | start = i_fold * fold_len 156 | if i_fold == self.n_fold -1: 157 | end = self.n_users + self.n_items 158 | else: 159 | end = (i_fold + 1) * fold_len 160 | 161 | A_fold_hat.append(self._convert_sp_mat_to_sp_tensor(X[start:end])) 162 | return A_fold_hat 163 | 164 | def _split_A_hat_node_dropout(self, X): 165 | A_fold_hat = [] 166 | 167 | fold_len = (self.n_users + self.n_items) // self.n_fold 168 | for i_fold in range(self.n_fold): 169 | start = i_fold * fold_len 170 | if i_fold == self.n_fold -1: 171 | end = self.n_users + self.n_items 172 | else: 173 | end = (i_fold + 1) * fold_len 174 | 175 | # A_fold_hat.append(self._convert_sp_mat_to_sp_tensor(X[start:end])) 176 | temp = self._convert_sp_mat_to_sp_tensor(X[start:end]) 177 | n_nonzero_temp = X[start:end].count_nonzero() 178 | A_fold_hat.append(self._dropout_sparse(temp, 1 - self.node_dropout[0], n_nonzero_temp)) 179 | 180 | return A_fold_hat 181 | 182 | def _create_ngcf_embed(self): 183 | # Generate a set of adjacency sub-matrix. 184 | if self.node_dropout_flag: 185 | # node dropout. 186 | A_fold_hat = self._split_A_hat_node_dropout(self.norm_adj) 187 | else: 188 | A_fold_hat = self._split_A_hat(self.norm_adj) 189 | 190 | ego_embeddings = tf.concat([self.weights['user_embedding'], self.weights['item_embedding']], axis=0) 191 | 192 | all_embeddings = [ego_embeddings] 193 | 194 | for k in range(0, self.n_layers): 195 | 196 | temp_embed = [] 197 | for f in range(self.n_fold): 198 | temp_embed.append(tf.sparse_tensor_dense_matmul(A_fold_hat[f], ego_embeddings)) 199 | 200 | # sum messages of neighbors. 201 | side_embeddings = tf.concat(temp_embed, 0) 202 | # transformed sum messages of neighbors. 203 | sum_embeddings = tf.nn.leaky_relu( 204 | tf.matmul(side_embeddings, self.weights['W_gc_%d' % k]) + self.weights['b_gc_%d' % k]) 205 | 206 | # bi messages of neighbors. 207 | bi_embeddings = tf.multiply(ego_embeddings, side_embeddings) 208 | # transformed bi messages of neighbors. 209 | bi_embeddings = tf.nn.leaky_relu( 210 | tf.matmul(bi_embeddings, self.weights['W_bi_%d' % k]) + self.weights['b_bi_%d' % k]) 211 | 212 | # non-linear activation. 213 | ego_embeddings = sum_embeddings + bi_embeddings 214 | 215 | # message dropout. 216 | ego_embeddings = tf.nn.dropout(ego_embeddings, 1 - self.mess_dropout[k]) 217 | 218 | # normalize the distribution of embeddings. 219 | norm_embeddings = tf.math.l2_normalize(ego_embeddings, axis=1) 220 | 221 | all_embeddings += [norm_embeddings] 222 | 223 | all_embeddings = tf.concat(all_embeddings, 1) 224 | u_g_embeddings, i_g_embeddings = tf.split(all_embeddings, [self.n_users, self.n_items], 0) 225 | return u_g_embeddings, i_g_embeddings 226 | 227 | def _create_gcn_embed(self): 228 | A_fold_hat = self._split_A_hat(self.norm_adj) 229 | embeddings = tf.concat([self.weights['user_embedding'], self.weights['item_embedding']], axis=0) 230 | 231 | 232 | all_embeddings = [embeddings] 233 | 234 | for k in range(0, self.n_layers): 235 | temp_embed = [] 236 | for f in range(self.n_fold): 237 | temp_embed.append(tf.sparse_tensor_dense_matmul(A_fold_hat[f], embeddings)) 238 | 239 | embeddings = tf.concat(temp_embed, 0) 240 | embeddings = tf.nn.leaky_relu(tf.matmul(embeddings, self.weights['W_gc_%d' %k]) + self.weights['b_gc_%d' %k]) 241 | embeddings = tf.nn.dropout(embeddings, 1 - self.mess_dropout[k]) 242 | 243 | all_embeddings += [embeddings] 244 | 245 | all_embeddings = tf.concat(all_embeddings, 1) 246 | u_g_embeddings, i_g_embeddings = tf.split(all_embeddings, [self.n_users, self.n_items], 0) 247 | return u_g_embeddings, i_g_embeddings 248 | 249 | def _create_gcmc_embed(self): 250 | A_fold_hat = self._split_A_hat(self.norm_adj) 251 | 252 | embeddings = tf.concat([self.weights['user_embedding'], self.weights['item_embedding']], axis=0) 253 | 254 | all_embeddings = [] 255 | 256 | for k in range(0, self.n_layers): 257 | temp_embed = [] 258 | for f in range(self.n_fold): 259 | temp_embed.append(tf.sparse_tensor_dense_matmul(A_fold_hat[f], embeddings)) 260 | embeddings = tf.concat(temp_embed, 0) 261 | # convolutional layer. 262 | embeddings = tf.nn.leaky_relu(tf.matmul(embeddings, self.weights['W_gc_%d' % k]) + self.weights['b_gc_%d' % k]) 263 | # dense layer. 264 | mlp_embeddings = tf.matmul(embeddings, self.weights['W_mlp_%d' %k]) + self.weights['b_mlp_%d' %k] 265 | mlp_embeddings = tf.nn.dropout(mlp_embeddings, 1 - self.mess_dropout[k]) 266 | 267 | all_embeddings += [mlp_embeddings] 268 | all_embeddings = tf.concat(all_embeddings, 1) 269 | 270 | u_g_embeddings, i_g_embeddings = tf.split(all_embeddings, [self.n_users, self.n_items], 0) 271 | return u_g_embeddings, i_g_embeddings 272 | 273 | 274 | def create_bpr_loss(self, users, pos_items, neg_items): 275 | pos_scores = tf.reduce_sum(tf.multiply(users, pos_items), axis=1) 276 | neg_scores = tf.reduce_sum(tf.multiply(users, neg_items), axis=1) 277 | 278 | regularizer = tf.nn.l2_loss(users) + tf.nn.l2_loss(pos_items) + tf.nn.l2_loss(neg_items) 279 | regularizer = regularizer/self.batch_size 280 | 281 | # In the first version, we implement the bpr loss via the following codes: 282 | # We report the performance in our paper using this implementation. 283 | maxi = tf.log(tf.nn.sigmoid(pos_scores - neg_scores)) 284 | mf_loss = tf.negative(tf.reduce_mean(maxi)) 285 | 286 | ## In the second version, we implement the bpr loss via the following codes to avoid 'NAN' loss during training: 287 | ## However, it will change the training performance and training performance. 288 | ## Please retrain the model and do a grid search for the best experimental setting. 289 | # mf_loss = tf.reduce_sum(tf.nn.softplus(-(pos_scores - neg_scores))) 290 | 291 | 292 | emb_loss = self.decay * regularizer 293 | 294 | reg_loss = tf.constant(0.0, tf.float32, [1]) 295 | 296 | return mf_loss, emb_loss, reg_loss 297 | 298 | def _convert_sp_mat_to_sp_tensor(self, X): 299 | coo = X.tocoo().astype(np.float32) 300 | indices = np.mat([coo.row, coo.col]).transpose() 301 | return tf.SparseTensor(indices, coo.data, coo.shape) 302 | 303 | def _dropout_sparse(self, X, keep_prob, n_nonzero_elems): 304 | """ 305 | Dropout for sparse tensors. 306 | """ 307 | noise_shape = [n_nonzero_elems] 308 | random_tensor = keep_prob 309 | random_tensor += tf.random_uniform(noise_shape) 310 | dropout_mask = tf.cast(tf.floor(random_tensor), dtype=tf.bool) 311 | pre_out = tf.sparse_retain(X, dropout_mask) 312 | 313 | return pre_out * tf.div(1., keep_prob) 314 | 315 | def load_pretrained_data(): 316 | pretrain_path = '%spretrain/%s/%s.npz' % (args.proj_path, args.dataset, 'embedding') 317 | try: 318 | pretrain_data = np.load(pretrain_path) 319 | print('load the pretrained embeddings.') 320 | except Exception: 321 | pretrain_data = None 322 | return pretrain_data 323 | 324 | if __name__ == '__main__': 325 | os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu_id) 326 | 327 | config = dict() 328 | config['n_users'] = data_generator.n_users 329 | config['n_items'] = data_generator.n_items 330 | 331 | """ 332 | ********************************************************* 333 | Generate the Laplacian matrix, where each entry defines the decay factor (e.g., p_ui) between two connected nodes. 334 | """ 335 | plain_adj, norm_adj, mean_adj = data_generator.get_adj_mat() 336 | 337 | if args.adj_type == 'plain': 338 | config['norm_adj'] = plain_adj 339 | print('use the plain adjacency matrix') 340 | 341 | elif args.adj_type == 'norm': 342 | config['norm_adj'] = norm_adj 343 | print('use the normalized adjacency matrix') 344 | 345 | elif args.adj_type == 'gcmc': 346 | config['norm_adj'] = mean_adj 347 | print('use the gcmc adjacency matrix') 348 | 349 | else: 350 | config['norm_adj'] = mean_adj + sp.eye(mean_adj.shape[0]) 351 | print('use the mean adjacency matrix') 352 | 353 | t0 = time() 354 | 355 | if args.pretrain == -1: 356 | pretrain_data = load_pretrained_data() 357 | else: 358 | pretrain_data = None 359 | 360 | model = NGCF(data_config=config, pretrain_data=pretrain_data) 361 | 362 | """ 363 | ********************************************************* 364 | Save the model parameters. 365 | """ 366 | saver = tf.train.Saver() 367 | 368 | if args.save_flag == 1: 369 | layer = '-'.join([str(l) for l in eval(args.layer_size)]) 370 | weights_save_path = '%sweights/%s/%s/%s/l%s_r%s' % (args.weights_path, args.dataset, model.model_type, layer, 371 | str(args.lr), '-'.join([str(r) for r in eval(args.regs)])) 372 | ensureDir(weights_save_path) 373 | save_saver = tf.train.Saver(max_to_keep=1) 374 | 375 | config = tf.ConfigProto() 376 | config.gpu_options.allow_growth = True 377 | sess = tf.Session(config=config) 378 | 379 | """ 380 | ********************************************************* 381 | Reload the pretrained model parameters. 382 | """ 383 | if args.pretrain == 1: 384 | layer = '-'.join([str(l) for l in eval(args.layer_size)]) 385 | 386 | pretrain_path = '%sweights/%s/%s/%s/l%s_r%s' % (args.weights_path, args.dataset, model.model_type, layer, 387 | str(args.lr), '-'.join([str(r) for r in eval(args.regs)])) 388 | 389 | 390 | ckpt = tf.train.get_checkpoint_state(os.path.dirname(pretrain_path + '/checkpoint')) 391 | if ckpt and ckpt.model_checkpoint_path: 392 | sess.run(tf.global_variables_initializer()) 393 | saver.restore(sess, ckpt.model_checkpoint_path) 394 | print('load the pretrained model parameters from: ', pretrain_path) 395 | 396 | # ********************************************************* 397 | # get the performance from pretrained model. 398 | if args.report != 1: 399 | users_to_test = list(data_generator.test_set.keys()) 400 | ret = test(sess, model, users_to_test, drop_flag=True) 401 | cur_best_pre_0 = ret['recall'][0] 402 | 403 | pretrain_ret = 'pretrained model recall=[%.5f, %.5f], precision=[%.5f, %.5f], hit=[%.5f, %.5f],' \ 404 | 'ndcg=[%.5f, %.5f]' % \ 405 | (ret['recall'][0], ret['recall'][-1], 406 | ret['precision'][0], ret['precision'][-1], 407 | ret['hit_ratio'][0], ret['hit_ratio'][-1], 408 | ret['ndcg'][0], ret['ndcg'][-1]) 409 | print(pretrain_ret) 410 | else: 411 | sess.run(tf.global_variables_initializer()) 412 | cur_best_pre_0 = 0. 413 | print('without pretraining.') 414 | 415 | else: 416 | sess.run(tf.global_variables_initializer()) 417 | cur_best_pre_0 = 0. 418 | print('without pretraining.') 419 | 420 | """ 421 | ********************************************************* 422 | Get the performance w.r.t. different sparsity levels. 423 | """ 424 | if args.report == 1: 425 | assert args.test_flag == 'full' 426 | users_to_test_list, split_state = data_generator.get_sparsity_split() 427 | users_to_test_list.append(list(data_generator.test_set.keys())) 428 | split_state.append('all') 429 | 430 | report_path = '%sreport/%s/%s.result' % (args.proj_path, args.dataset, model.model_type) 431 | ensureDir(report_path) 432 | f = open(report_path, 'w') 433 | f.write( 434 | 'embed_size=%d, lr=%.4f, layer_size=%s, keep_prob=%s, regs=%s, loss_type=%s, adj_type=%s\n' 435 | % (args.embed_size, args.lr, args.layer_size, args.keep_prob, args.regs, args.loss_type, args.adj_type)) 436 | 437 | for i, users_to_test in enumerate(users_to_test_list): 438 | ret = test(sess, model, users_to_test, drop_flag=True) 439 | 440 | final_perf = "recall=[%s], precision=[%s], hit=[%s], ndcg=[%s]" % \ 441 | ('\t'.join(['%.5f' % r for r in ret['recall']]), 442 | '\t'.join(['%.5f' % r for r in ret['precision']]), 443 | '\t'.join(['%.5f' % r for r in ret['hit_ratio']]), 444 | '\t'.join(['%.5f' % r for r in ret['ndcg']])) 445 | print(final_perf) 446 | 447 | f.write('\t%s\n\t%s\n' % (split_state[i], final_perf)) 448 | f.close() 449 | exit() 450 | 451 | """ 452 | ********************************************************* 453 | Train. 454 | """ 455 | loss_loger, pre_loger, rec_loger, ndcg_loger, hit_loger = [], [], [], [], [] 456 | stopping_step = 0 457 | should_stop = False 458 | 459 | for epoch in range(args.epoch): 460 | t1 = time() 461 | loss, mf_loss, emb_loss, reg_loss = 0., 0., 0., 0. 462 | n_batch = data_generator.n_train // args.batch_size + 1 463 | 464 | for idx in range(n_batch): 465 | users, pos_items, neg_items = data_generator.sample() 466 | _, batch_loss, batch_mf_loss, batch_emb_loss, batch_reg_loss = sess.run([model.opt, model.loss, model.mf_loss, model.emb_loss, model.reg_loss], 467 | feed_dict={model.users: users, model.pos_items: pos_items, 468 | model.node_dropout: eval(args.node_dropout), 469 | model.mess_dropout: eval(args.mess_dropout), 470 | model.neg_items: neg_items}) 471 | loss += batch_loss 472 | mf_loss += batch_mf_loss 473 | emb_loss += batch_emb_loss 474 | reg_loss += batch_reg_loss 475 | 476 | if np.isnan(loss) == True: 477 | print('ERROR: loss is nan.') 478 | sys.exit() 479 | 480 | # print the test evaluation metrics each 10 epochs; pos:neg = 1:10. 481 | if (epoch + 1) % 10 != 0: 482 | if args.verbose > 0 and epoch % args.verbose == 0: 483 | perf_str = 'Epoch %d [%.1fs]: train==[%.5f=%.5f + %.5f]' % ( 484 | epoch, time() - t1, loss, mf_loss, reg_loss) 485 | print(perf_str) 486 | continue 487 | 488 | t2 = time() 489 | users_to_test = list(data_generator.test_set.keys()) 490 | ret = test(sess, model, users_to_test, drop_flag=True) 491 | 492 | t3 = time() 493 | 494 | loss_loger.append(loss) 495 | rec_loger.append(ret['recall']) 496 | pre_loger.append(ret['precision']) 497 | ndcg_loger.append(ret['ndcg']) 498 | hit_loger.append(ret['hit_ratio']) 499 | 500 | if args.verbose > 0: 501 | perf_str = 'Epoch %d [%.1fs + %.1fs]: train==[%.5f=%.5f + %.5f + %.5f], recall=[%.5f, %.5f], ' \ 502 | 'precision=[%.5f, %.5f], hit=[%.5f, %.5f], ndcg=[%.5f, %.5f]' % \ 503 | (epoch, t2 - t1, t3 - t2, loss, mf_loss, emb_loss, reg_loss, ret['recall'][0], ret['recall'][-1], 504 | ret['precision'][0], ret['precision'][-1], ret['hit_ratio'][0], ret['hit_ratio'][-1], 505 | ret['ndcg'][0], ret['ndcg'][-1]) 506 | print(perf_str) 507 | 508 | cur_best_pre_0, stopping_step, should_stop = early_stopping(ret['recall'][0], cur_best_pre_0, 509 | stopping_step, expected_order='acc', flag_step=5) 510 | 511 | # ********************************************************* 512 | # early stopping when cur_best_pre_0 is decreasing for ten successive steps. 513 | if should_stop == True: 514 | break 515 | 516 | # ********************************************************* 517 | # save the user & item embeddings for pretraining. 518 | if ret['recall'][0] == cur_best_pre_0 and args.save_flag == 1: 519 | save_saver.save(sess, weights_save_path + '/weights', global_step=epoch) 520 | print('save the weights in path: ', weights_save_path) 521 | 522 | recs = np.array(rec_loger) 523 | pres = np.array(pre_loger) 524 | ndcgs = np.array(ndcg_loger) 525 | hit = np.array(hit_loger) 526 | 527 | best_rec_0 = max(recs[:, 0]) 528 | idx = list(recs[:, 0]).index(best_rec_0) 529 | 530 | final_perf = "Best Iter=[%d]@[%.1f]\trecall=[%s], precision=[%s], hit=[%s], ndcg=[%s]" % \ 531 | (idx, time() - t0, '\t'.join(['%.5f' % r for r in recs[idx]]), 532 | '\t'.join(['%.5f' % r for r in pres[idx]]), 533 | '\t'.join(['%.5f' % r for r in hit[idx]]), 534 | '\t'.join(['%.5f' % r for r in ndcgs[idx]])) 535 | print(final_perf) 536 | 537 | save_path = '%soutput/%s/%s.result' % (args.proj_path, args.dataset, model.model_type) 538 | ensureDir(save_path) 539 | f = open(save_path, 'a') 540 | 541 | f.write( 542 | 'embed_size=%d, lr=%.4f, layer_size=%s, node_dropout=%s, mess_dropout=%s, regs=%s, adj_type=%s\n\t%s\n' 543 | % (args.embed_size, args.lr, args.layer_size, args.node_dropout, args.mess_dropout, args.regs, 544 | args.adj_type, final_perf)) 545 | f.close() 546 | -------------------------------------------------------------------------------- /NGCF/NMF.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Oct 10, 2018 3 | Tensorflow Implementation of the baseline of "Neural Collaborative Filtering, He et al. SIGIR 2017" in: 4 | Wang Xiang et al. Neural Graph Collaborative Filtering. In SIGIR 2019. 5 | @author: Xiang Wang (xiangwang@u.nus.edu) 6 | ''' 7 | import tensorflow as tf 8 | from utility.helper import * 9 | import numpy as np 10 | from scipy.sparse import csr_matrix 11 | from utility.batch_test import * 12 | import os 13 | import sys 14 | from tensorflow.contrib.layers.python.layers import batch_norm as batch_norm 15 | os.environ['TF_CPP_MIN_LOG_LEVEL']='2' 16 | 17 | class NMF(object): 18 | def __init__(self, data_config, pretrain_data): 19 | self.model_type = 'nmf' 20 | self.pretrain_data = pretrain_data 21 | 22 | self.n_users = data_config['n_users'] 23 | self.n_items = data_config['n_items'] 24 | 25 | self.lr = args.lr 26 | # self.lr_decay = args.lr_decay 27 | 28 | self.emb_dim = args.embed_size 29 | self.batch_size = args.batch_size 30 | 31 | self.weight_size = eval(args.layer_size) 32 | self.n_layers = len(self.weight_size) 33 | 34 | self.model_type += '_l%d' % self.n_layers 35 | 36 | self.regs = eval(args.regs) 37 | self.decay = self.regs[-1] 38 | 39 | self.verbose = args.verbose 40 | 41 | # placeholder definition 42 | self.users = tf.placeholder(tf.int32, shape=(None)) 43 | self.pos_items = tf.placeholder(tf.int32, shape=(None)) 44 | self.neg_items = tf.placeholder(tf.int32, shape=(None)) 45 | 46 | self.dropout_keep = tf.placeholder(tf.float32, shape=[None]) 47 | self.train_phase = tf.placeholder(tf.bool) 48 | 49 | # self.global_step = tf.Variable(0, trainable=False) 50 | self.weights = self._init_weights() 51 | 52 | 53 | # Original embedding. 54 | u_e = tf.nn.embedding_lookup(self.weights['user_embedding'], self.users) 55 | pos_i_e = tf.nn.embedding_lookup(self.weights['item_embedding'], self.pos_items) 56 | neg_i_e = tf.nn.embedding_lookup(self.weights['item_embedding'], self.neg_items) 57 | 58 | # All ratings for all users. 59 | self.batch_ratings = self._create_batch_ratings(u_e, pos_i_e) 60 | 61 | self.mf_loss, self.emb_loss, self.reg_loss = self.create_bpr_loss(u_e, pos_i_e, neg_i_e) 62 | self.loss = self.mf_loss + self.emb_loss + self.reg_loss 63 | 64 | # self.dy_lr = tf.train.exponential_decay(self.lr, self.global_step, 10000, self.lr_decay, staircase=True) 65 | # self.opt = tf.train.RMSPropOptimizer(learning_rate=self.dy_lr).minimize(self.loss, global_step=self.global_step) 66 | self.opt = tf.train.RMSPropOptimizer(learning_rate=self.lr).minimize(self.loss) 67 | # self.updates = self.opt.minimize(self.loss, var_list=self.weights) 68 | 69 | self._statistics_params() 70 | 71 | def _init_weights(self): 72 | all_weights = dict() 73 | 74 | initializer = tf.contrib.layers.xavier_initializer() 75 | 76 | 77 | if self.pretrain_data is None: 78 | all_weights['user_embedding'] = tf.Variable(initializer([self.n_users, self.emb_dim]), name='user_embedding') 79 | all_weights['item_embedding'] = tf.Variable(initializer([self.n_items, self.emb_dim]), name='item_embedding') 80 | print('using xavier initialization') 81 | else: 82 | all_weights['user_embedding'] = tf.Variable(initial_value=self.pretrain_data['user_embed'], trainable=True, 83 | name='user_embedding', dtype=tf.float32) 84 | all_weights['item_embedding'] = tf.Variable(initial_value=self.pretrain_data['item_embed'], trainable=True, 85 | name='item_embedding', dtype=tf.float32) 86 | print('using pretrained initialization') 87 | 88 | 89 | if self.model_type == 'mlp': 90 | self.weight_size_list = [2 * self.emb_dim] + self.weight_size 91 | elif self.model_type == 'jrl': 92 | self.weight_size_list = [self.emb_dim] + self.weight_size 93 | else: 94 | self.weight_size_list = [3 * self.emb_dim] + self.weight_size 95 | 96 | for i in range(self.n_layers): 97 | all_weights['W_%d' %i] = tf.Variable( 98 | initializer([self.weight_size_list[i], self.weight_size_list[i+1]]), name='W_%d' %i) 99 | all_weights['b_%d' %i] = tf.Variable( 100 | initializer([1, self.weight_size_list[i+1]]), name='b_%d' %i) 101 | 102 | all_weights['h'] = tf.Variable(initializer([self.weight_size_list[-1], 1]), name='h') 103 | 104 | return all_weights 105 | 106 | def create_bpr_loss(self, users, pos_items, neg_items): 107 | pos_scores = self._create_inference(users, pos_items) 108 | neg_scores = self._create_inference(users, neg_items) 109 | 110 | regularizer = tf.nn.l2_loss(users) + tf.nn.l2_loss(pos_items) + tf.nn.l2_loss(neg_items) 111 | regularizer = regularizer/self.batch_size 112 | 113 | maxi = tf.log(tf.nn.sigmoid(pos_scores - neg_scores)) 114 | mf_loss = tf.negative(tf.reduce_mean(maxi)) 115 | 116 | emb_loss = self.regs[-1] * regularizer 117 | 118 | reg_loss = self.regs[-2] * tf.nn.l2_loss(self.weights['h']) 119 | 120 | return mf_loss, emb_loss, reg_loss 121 | 122 | def _create_inference(self, u_e, i_e): 123 | z = [] 124 | 125 | if self.model_type == 'mlp': 126 | z.append(tf.concat([u_e, i_e], 1)) 127 | elif self.model_type == 'jrl': 128 | z.append(u_e * i_e) 129 | else: 130 | z.append(tf.concat([u_e, i_e, u_e * i_e], 1)) 131 | 132 | # z[0] = self.batch_norm_layer(z[0], train_phase=self.train_phase, scope_bn='bn_mlp') 133 | 134 | for i in range(self.n_layers): 135 | # (batch, W[i]) * (W[i], W[i+1]) + (1, W[i+1]) => (batch, W[i+1]) 136 | # temp = self.batch_norm_layer(z[i], train_phase=self.train_phase, scope_bn='mlp_%d' % i) 137 | 138 | temp = tf.nn.relu(tf.matmul(z[i], self.weights['W_%d' % i]) + self.weights['b_%d' % i]) 139 | temp = tf.nn.dropout(temp, self.dropout_keep[i]) 140 | z.append(temp) 141 | 142 | agg_out = tf.matmul(z[-1], self.weights['h']) 143 | return agg_out 144 | 145 | def _create_all_ratings(self, u_e): 146 | z = [] 147 | 148 | if self.model_type == 'jrl': 149 | u_1 = tf.expand_dims(u_e, axis=1) 150 | i_1 = tf.expand_dims(self.weights['item_embedding'], axis=0) 151 | z.append(tf.reshape(u_1 * i_1, [-1, self.emb_dim])) 152 | 153 | elif self.model_type == 'mlp': 154 | u_1 = tf.reshape(tf.tile(u_e, [1, self.n_items]), [-1, self.emb_dim]) 155 | i_1 = tf.tile(self.weights['item_embedding'], [self.batch_size, 1]) 156 | z.append(tf.concat([u_1, i_1], 1)) 157 | else: 158 | u_1 = tf.expand_dims(u_e, axis=1) 159 | i_1 = tf.expand_dims(self.weights['item_embedding'], axis=0) 160 | u_i = tf.reshape(u_1 * i_1, [-1, self.emb_dim]) 161 | 162 | u_1 = tf.reshape(tf.tile(u_e, [1, self.n_items]), [-1, self.emb_dim]) 163 | i_1 = tf.tile(self.weights['item_embedding'], [self.batch_size, 1]) 164 | z.append(tf.concat([u_1, i_1, u_i], 1)) 165 | 166 | for i in range(self.n_layers): 167 | # (batch, W[i]) * (W[i], W[i+1]) + (1, W[i+1]) => (batch, W[i+1]) 168 | z.append(tf.nn.relu(tf.matmul(z[i], self.weights['W_%d' % i]) + self.weights['b_%d' % i])) 169 | 170 | agg_out = tf.matmul(z[-1], self.weights['h']) # (batch, W[-1]) * (W[-1], 1) => (batch, 1) 171 | all_ratings = tf.reshape(agg_out, [-1, self.n_items]) 172 | return all_ratings 173 | 174 | def _create_batch_ratings(self, u_e, i_e): 175 | z = [] 176 | 177 | n_b_user = tf.shape(u_e)[0] 178 | n_b_item = tf.shape(i_e)[0] 179 | 180 | 181 | if self.model_type == 'jrl': 182 | u_1 = tf.expand_dims(u_e, axis=1) 183 | i_1 = tf.expand_dims(i_e, axis=0) 184 | z.append(tf.reshape(u_1 * i_1, [-1, self.emb_dim])) # (n_b_user * n_b_item, embed_size) 185 | 186 | elif self.model_type == 'mlp': 187 | u_1 = tf.reshape(tf.tile(u_e, [1, n_b_item]), [-1, self.emb_dim]) 188 | i_1 = tf.tile(i_e, [n_b_user, 1]) 189 | z.append(tf.concat([u_1, i_1], 1)) # (n_b_user * n_b_item, 2*embed_size) 190 | else: 191 | u_1 = tf.expand_dims(u_e, axis=1) 192 | i_1 = tf.expand_dims(i_e, axis=0) 193 | u_i = tf.reshape(u_1 * i_1, [-1, self.emb_dim]) 194 | 195 | u_1 = tf.reshape(tf.tile(u_e, [1, n_b_item]), [-1, self.emb_dim]) 196 | i_1 = tf.tile(i_e, [n_b_user, 1]) 197 | z.append(tf.concat([u_1, i_1, u_i], 1)) 198 | 199 | for i in range(self.n_layers): 200 | # (batch, W[i]) * (W[i], W[i+1]) + (1, W[i+1]) => (batch, W[i+1]) 201 | z.append(tf.nn.relu(tf.matmul(z[i], self.weights['W_%d' % i]) + self.weights['b_%d' % i])) 202 | 203 | agg_out = tf.matmul(z[-1], self.weights['h']) # (batch, W[-1]) * (W[-1], 1) => (batch, 1) 204 | batch_ratings = tf.reshape(agg_out, [n_b_user, n_b_item]) 205 | return batch_ratings 206 | 207 | def batch_norm_layer(self, x, train_phase, scope_bn): 208 | with tf.variable_scope(scope_bn): 209 | return batch_norm(x, decay=0.9, center=True, scale=True, updates_collections=None, 210 | is_training=True, reuse=tf.AUTO_REUSE, trainable=True, scope=scope_bn) 211 | 212 | def _statistics_params(self): 213 | # number of params 214 | total_parameters = 0 215 | for variable in self.weights.values(): 216 | shape = variable.get_shape() # shape is an array of tf.Dimension 217 | variable_parameters = 1 218 | for dim in shape: 219 | variable_parameters *= dim.value 220 | total_parameters += variable_parameters 221 | if self.verbose > 0: 222 | print("#params: %d" % total_parameters) 223 | 224 | def load_pretrained_data(): 225 | pretrain_path = '%spretrain/%s/%s.npz' % (args.proj_path, args.dataset, 'bprmf') 226 | pretrain_data = np.load(pretrain_path) 227 | print('load the pretrained bprmf model parameters.') 228 | return pretrain_data 229 | 230 | if __name__ == '__main__': 231 | # os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu_id) 232 | 233 | config = dict() 234 | config['n_users'] = data_generator.n_users 235 | config['n_items'] = data_generator.n_items 236 | 237 | t0 = time() 238 | 239 | if args.pretrain == -1: 240 | pretrain_data = load_pretrained_data() 241 | else: 242 | pretrain_data = None 243 | 244 | 245 | model = NMF(data_config=config, pretrain_data=pretrain_data) 246 | 247 | saver = tf.train.Saver() 248 | # ********************************************************* 249 | # save the model parameters. 250 | if args.save_flag == 1: 251 | layer = '-'.join([str(l) for l in eval(args.layer_size)]) 252 | weights_save_path = '%sweights/%s/%s/%s/l%s_r%s' % (args.proj_path, args.dataset, model.model_type, layer, str(args.lr), 253 | '-'.join([str(r) for r in eval(args.regs)])) 254 | ensureDir(weights_save_path) 255 | save_saver = tf.train.Saver(max_to_keep=1) 256 | 257 | config = tf.ConfigProto() 258 | config.gpu_options.allow_growth = True 259 | sess = tf.Session(config=config) 260 | 261 | # ********************************************************* 262 | # reload the pretrained model parameters. 263 | if args.pretrain == 1: 264 | layer = '-'.join([str(l) for l in eval(args.layer_size)]) 265 | pretrain_path = '%sweights/%s/%s/%s/l%s_r%s' % (args.proj_path, args.dataset, model.model_type, layer, str(args.lr), 266 | '-'.join([str(r) for r in eval(args.regs)])) 267 | ckpt = tf.train.get_checkpoint_state(os.path.dirname(pretrain_path + '/checkpoint')) 268 | if ckpt and ckpt.model_checkpoint_path: 269 | sess.run(tf.global_variables_initializer()) 270 | saver.restore(sess, ckpt.model_checkpoint_path) 271 | print('load the pretrained model parameters from: ', pretrain_path) 272 | 273 | # ********************************************************* 274 | # get the performance from pretrained model. 275 | users_to_test = list(data_generator.test_set.keys()) 276 | ret = test(sess, model, users_to_test, drop_flag=True) 277 | cur_best_pre_0 = ret['recall'][0] 278 | 279 | pretrain_ret = 'pretrained model recall=[%.5f, %.5f], precision=[%.5f, %.5f], hit=[%.5f, %.5f],' \ 280 | 'ndcg=[%.5f, %.5f], auc=[%.5f]' % \ 281 | (ret['recall'][0], ret['recall'][-1], 282 | ret['precision'][0], ret['precision'][-1], 283 | ret['hit_ratio'][0], ret['hit_ratio'][-1], 284 | ret['ndcg'][0], ret['ndcg'][-1], ret['auc']) 285 | print(pretrain_ret) 286 | else: 287 | sess.run(tf.global_variables_initializer()) 288 | cur_best_pre_0 = 0. 289 | print('without pretraining.') 290 | 291 | else: 292 | sess.run(tf.global_variables_initializer()) 293 | cur_best_pre_0 = 0. 294 | print('without pretraining.') 295 | 296 | loss_loger, pre_loger, rec_loger, ndcg_loger, auc_loger, hit_loger = [], [], [], [], [], [] 297 | stopping_step = 0 298 | should_stop = False 299 | 300 | for epoch in range(args.epoch): 301 | t1 = time() 302 | loss, mf_loss, emb_loss, reg_loss = 0., 0., 0., 0. 303 | n_batch = data_generator.n_train // args.batch_size + 1 304 | 305 | 306 | for idx in range(n_batch): 307 | users, pos_items, neg_items = data_generator.sample() 308 | _, batch_loss, batch_mf_loss, batch_emb_loss, batch_reg_loss = sess.run([model.opt, model.loss, model.mf_loss, model.emb_loss, model.reg_loss], 309 | feed_dict={model.users: users, model.pos_items: pos_items, 310 | model.neg_items: neg_items, 311 | model.dropout_keep: eval(args.keep_prob), 312 | model.train_phase: True}) 313 | loss += batch_loss 314 | mf_loss += batch_mf_loss 315 | emb_loss += batch_emb_loss 316 | reg_loss += batch_reg_loss 317 | 318 | if np.isnan(loss) == True: 319 | print('ERROR: loss is nan.') 320 | sys.exit() 321 | 322 | # print the test evaluation metrics each 10 epochs; pos:neg = 1:10. 323 | if (epoch + 1) % 10 != 0: 324 | if args.verbose > 0 and epoch % args.verbose == 0: 325 | perf_str = 'Epoch %d [%.1fs]: train==[%.5f=%.5f + %.5f]' % (epoch, time() - t1, loss, mf_loss, reg_loss) 326 | print(perf_str) 327 | continue 328 | 329 | t2 = time() 330 | users_to_test = list(data_generator.test_set.keys()) 331 | ret = test(sess, model, users_to_test, batch_test_flag=True) 332 | 333 | t3 = time() 334 | 335 | loss_loger.append(loss) 336 | rec_loger.append(ret['recall']) 337 | pre_loger.append(ret['precision']) 338 | ndcg_loger.append(ret['ndcg']) 339 | auc_loger.append(ret['auc']) 340 | hit_loger.append(ret['hit_ratio']) 341 | 342 | if args.verbose > 0: 343 | perf_str = 'Epoch %d [%.1fs + %.1fs]: train==[%.5f=%.5f + %.5f + %.5f], recall=[%.5f, %.5f], ' \ 344 | 'precision=[%.5f, %.5f], hit=[%.5f, %.5f], ndcg=[%.5f, %.5f], auc=[%.5f]' % \ 345 | (epoch, t2 - t1, t3 - t2, loss, mf_loss, emb_loss, reg_loss, ret['recall'][0], ret['recall'][-1], 346 | ret['precision'][0], ret['precision'][-1], ret['hit_ratio'][0], ret['hit_ratio'][-1], 347 | ret['ndcg'][0], ret['ndcg'][-1], ret['auc']) 348 | print(perf_str) 349 | 350 | cur_best_pre_0, stopping_step, should_stop = early_stopping(ret['recall'][0], cur_best_pre_0, 351 | stopping_step, expected_order='acc', flag_step=5) 352 | 353 | # ********************************************************* 354 | # early stopping when cur_best_pre_0 is decreasing for ten successive steps. 355 | if should_stop == True: 356 | break 357 | 358 | # ********************************************************* 359 | # save the user & item embeddings for pretraining. 360 | if ret['recall'][0] == cur_best_pre_0 and args.save_flag == 1: 361 | save_saver.save(sess, weights_save_path + '/weights', global_step=epoch) 362 | print('save the weights in path: ', weights_save_path) 363 | 364 | recs = np.array(rec_loger) 365 | pres = np.array(pre_loger) 366 | ndcgs = np.array(ndcg_loger) 367 | auc = np.array(auc_loger) 368 | hit = np.array(hit_loger) 369 | 370 | best_rec_0 = max(recs[:, 0]) 371 | idx = list(recs[:, 0]).index(best_rec_0) 372 | 373 | final_perf = "Best Iter=[%d]@[%.1f]\trecall=[%s], precision=[%s], hit=[%s], ndcg=[%s], auc=[%.5f]" % \ 374 | (idx, time() - t0, '\t'.join(['%.5f' % r for r in recs[idx]]), 375 | '\t'.join(['%.5f' % r for r in pres[idx]]), 376 | '\t'.join(['%.5f' % r for r in hit[idx]]), 377 | '\t'.join(['%.5f' % r for r in ndcgs[idx]]), 378 | auc[idx]) 379 | print(final_perf) 380 | 381 | save_path = '%soutput/%s/%s.result' % (args.proj_path, args.dataset, model.model_type) 382 | ensureDir(save_path) 383 | f = open(save_path, 'a') 384 | 385 | f.write('embed_size=%d, lr=%.4f, layer_size=%s, keep_prob=%s, regs=%s, loss_type=%s, \n\t%s\n' 386 | % (args.embed_size, args.lr, args.layer_size, args.keep_prob, args.regs, args.loss_type, final_perf)) 387 | f.close() 388 | -------------------------------------------------------------------------------- /NGCF/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /NGCF/utility/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /NGCF/utility/batch_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Oct 10, 2018 3 | Tensorflow Implementation of Neural Graph Collaborative Filtering (NGCF) model in: 4 | Wang Xiang et al. Neural Graph Collaborative Filtering. In SIGIR 2019. 5 | 6 | @author: Xiang Wang (xiangwang@u.nus.edu) 7 | ''' 8 | import utility.metrics as metrics 9 | from utility.parser import parse_args 10 | from utility.load_data import * 11 | import multiprocessing 12 | import heapq 13 | 14 | cores = multiprocessing.cpu_count() // 2 15 | 16 | args = parse_args() 17 | Ks = eval(args.Ks) 18 | 19 | data_generator = Data(path=args.data_path + args.dataset, batch_size=args.batch_size) 20 | USR_NUM, ITEM_NUM = data_generator.n_users, data_generator.n_items 21 | N_TRAIN, N_TEST = data_generator.n_train, data_generator.n_test 22 | BATCH_SIZE = args.batch_size 23 | 24 | def ranklist_by_heapq(user_pos_test, test_items, rating, Ks): 25 | item_score = {} 26 | for i in test_items: 27 | item_score[i] = rating[i] 28 | 29 | K_max = max(Ks) 30 | K_max_item_score = heapq.nlargest(K_max, item_score, key=item_score.get) 31 | 32 | r = [] 33 | for i in K_max_item_score: 34 | if i in user_pos_test: 35 | r.append(1) 36 | else: 37 | r.append(0) 38 | auc = 0. 39 | return r, auc 40 | 41 | def get_auc(item_score, user_pos_test): 42 | item_score = sorted(item_score.items(), key=lambda kv: kv[1]) 43 | item_score.reverse() 44 | item_sort = [x[0] for x in item_score] 45 | posterior = [x[1] for x in item_score] 46 | 47 | r = [] 48 | for i in item_sort: 49 | if i in user_pos_test: 50 | r.append(1) 51 | else: 52 | r.append(0) 53 | auc = metrics.auc(ground_truth=r, prediction=posterior) 54 | return auc 55 | 56 | def ranklist_by_sorted(user_pos_test, test_items, rating, Ks): 57 | item_score = {} 58 | for i in test_items: 59 | item_score[i] = rating[i] 60 | 61 | K_max = max(Ks) 62 | K_max_item_score = heapq.nlargest(K_max, item_score, key=item_score.get) 63 | 64 | r = [] 65 | for i in K_max_item_score: 66 | if i in user_pos_test: 67 | r.append(1) 68 | else: 69 | r.append(0) 70 | auc = get_auc(item_score, user_pos_test) 71 | return r, auc 72 | 73 | def get_performance(user_pos_test, r, auc, Ks): 74 | precision, recall, ndcg, hit_ratio = [], [], [], [] 75 | 76 | for K in Ks: 77 | precision.append(metrics.precision_at_k(r, K)) 78 | recall.append(metrics.recall_at_k(r, K, len(user_pos_test))) 79 | ndcg.append(metrics.ndcg_at_k(r, K)) 80 | hit_ratio.append(metrics.hit_at_k(r, K)) 81 | 82 | return {'recall': np.array(recall), 'precision': np.array(precision), 83 | 'ndcg': np.array(ndcg), 'hit_ratio': np.array(hit_ratio), 'auc': auc} 84 | 85 | 86 | def test_one_user(x): 87 | # user u's ratings for user u 88 | rating = x[0] 89 | #uid 90 | u = x[1] 91 | #user u's items in the training set 92 | try: 93 | training_items = data_generator.train_items[u] 94 | except Exception: 95 | training_items = [] 96 | #user u's items in the test set 97 | user_pos_test = data_generator.test_set[u] 98 | 99 | all_items = set(range(ITEM_NUM)) 100 | 101 | test_items = list(all_items - set(training_items)) 102 | 103 | if args.test_flag == 'part': 104 | r, auc = ranklist_by_heapq(user_pos_test, test_items, rating, Ks) 105 | else: 106 | r, auc = ranklist_by_sorted(user_pos_test, test_items, rating, Ks) 107 | 108 | return get_performance(user_pos_test, r, auc, Ks) 109 | 110 | 111 | def test(sess, model, users_to_test, drop_flag=False, batch_test_flag=False): 112 | result = {'precision': np.zeros(len(Ks)), 'recall': np.zeros(len(Ks)), 'ndcg': np.zeros(len(Ks)), 113 | 'hit_ratio': np.zeros(len(Ks)), 'auc': 0.} 114 | 115 | pool = multiprocessing.Pool(cores) 116 | 117 | u_batch_size = BATCH_SIZE * 2 118 | i_batch_size = BATCH_SIZE 119 | 120 | test_users = users_to_test 121 | n_test_users = len(test_users) 122 | n_user_batchs = n_test_users // u_batch_size + 1 123 | 124 | count = 0 125 | 126 | for u_batch_id in range(n_user_batchs): 127 | start = u_batch_id * u_batch_size 128 | end = (u_batch_id + 1) * u_batch_size 129 | 130 | user_batch = test_users[start: end] 131 | 132 | if batch_test_flag: 133 | 134 | n_item_batchs = ITEM_NUM // i_batch_size + 1 135 | rate_batch = np.zeros(shape=(len(user_batch), ITEM_NUM)) 136 | 137 | i_count = 0 138 | for i_batch_id in range(n_item_batchs): 139 | i_start = i_batch_id * i_batch_size 140 | i_end = min((i_batch_id + 1) * i_batch_size, ITEM_NUM) 141 | 142 | item_batch = range(i_start, i_end) 143 | 144 | if drop_flag == False: 145 | i_rate_batch = sess.run(model.batch_ratings, {model.users: user_batch, 146 | model.pos_items: item_batch}) 147 | else: 148 | i_rate_batch = sess.run(model.batch_ratings, {model.users: user_batch, 149 | model.pos_items: item_batch, 150 | model.node_dropout: [0.]*len(eval(args.layer_size)), 151 | model.mess_dropout: [0.]*len(eval(args.layer_size))}) 152 | rate_batch[:, i_start: i_end] = i_rate_batch 153 | i_count += i_rate_batch.shape[1] 154 | 155 | assert i_count == ITEM_NUM 156 | 157 | else: 158 | item_batch = range(ITEM_NUM) 159 | 160 | if drop_flag == False: 161 | rate_batch = sess.run(model.batch_ratings, {model.users: user_batch, 162 | model.pos_items: item_batch}) 163 | else: 164 | rate_batch = sess.run(model.batch_ratings, {model.users: user_batch, 165 | model.pos_items: item_batch, 166 | model.node_dropout: [0.] * len(eval(args.layer_size)), 167 | model.mess_dropout: [0.] * len(eval(args.layer_size))}) 168 | 169 | user_batch_rating_uid = zip(rate_batch, user_batch) 170 | batch_result = pool.map(test_one_user, user_batch_rating_uid) 171 | count += len(batch_result) 172 | 173 | for re in batch_result: 174 | result['precision'] += re['precision']/n_test_users 175 | result['recall'] += re['recall']/n_test_users 176 | result['ndcg'] += re['ndcg']/n_test_users 177 | result['hit_ratio'] += re['hit_ratio']/n_test_users 178 | result['auc'] += re['auc']/n_test_users 179 | 180 | 181 | assert count == n_test_users 182 | pool.close() 183 | return result 184 | -------------------------------------------------------------------------------- /NGCF/utility/helper.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Aug 19, 2016 3 | @author: Xiang Wang (xiangwang@u.nus.edu) 4 | ''' 5 | __author__ = "xiangwang" 6 | import os 7 | import re 8 | 9 | def txt2list(file_src): 10 | orig_file = open(file_src, "r") 11 | lines = orig_file.readlines() 12 | return lines 13 | 14 | def ensureDir(dir_path): 15 | d = os.path.dirname(dir_path) 16 | if not os.path.exists(d): 17 | os.makedirs(d) 18 | 19 | def uni2str(unicode_str): 20 | return str(unicode_str.encode('ascii', 'ignore')).replace('\n', '').strip() 21 | 22 | def hasNumbers(inputString): 23 | return bool(re.search(r'\d', inputString)) 24 | 25 | def delMultiChar(inputString, chars): 26 | for ch in chars: 27 | inputString = inputString.replace(ch, '') 28 | return inputString 29 | 30 | def merge_two_dicts(x, y): 31 | z = x.copy() # start with x's keys and values 32 | z.update(y) # modifies z with y's keys and values & returns None 33 | return z 34 | 35 | def early_stopping(log_value, best_value, stopping_step, expected_order='acc', flag_step=100): 36 | # early stopping strategy: 37 | assert expected_order in ['acc', 'dec'] 38 | 39 | if (expected_order == 'acc' and log_value >= best_value) or (expected_order == 'dec' and log_value <= best_value): 40 | stopping_step = 0 41 | best_value = log_value 42 | else: 43 | stopping_step += 1 44 | 45 | if stopping_step >= flag_step: 46 | print("Early stopping is trigger at step: {} log:{}".format(flag_step, log_value)) 47 | should_stop = True 48 | else: 49 | should_stop = False 50 | return best_value, stopping_step, should_stop 51 | -------------------------------------------------------------------------------- /NGCF/utility/load_data.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Oct 10, 2018 3 | Tensorflow Implementation of Neural Graph Collaborative Filtering (NGCF) model in: 4 | Wang Xiang et al. Neural Graph Collaborative Filtering. In SIGIR 2019. 5 | 6 | @author: Xiang Wang (xiangwang@u.nus.edu) 7 | ''' 8 | import numpy as np 9 | import random as rd 10 | import scipy.sparse as sp 11 | from time import time 12 | 13 | class Data(object): 14 | def __init__(self, path, batch_size): 15 | self.path = path 16 | self.batch_size = batch_size 17 | 18 | train_file = path + '/train.txt' 19 | test_file = path + '/test.txt' 20 | 21 | #get number of users and items 22 | self.n_users, self.n_items = 0, 0 23 | self.n_train, self.n_test = 0, 0 24 | self.neg_pools = {} 25 | 26 | self.exist_users = [] 27 | 28 | with open(train_file) as f: 29 | for l in f.readlines(): 30 | if len(l) > 0: 31 | l = l.strip('\n').split(' ') 32 | items = [int(i) for i in l[1:]] 33 | uid = int(l[0]) 34 | self.exist_users.append(uid) 35 | self.n_items = max(self.n_items, max(items)) 36 | self.n_users = max(self.n_users, uid) 37 | self.n_train += len(items) 38 | 39 | with open(test_file) as f: 40 | for l in f.readlines(): 41 | if len(l) > 0: 42 | l = l.strip('\n') 43 | try: 44 | items = [int(i) for i in l.split(' ')[1:]] 45 | except Exception: 46 | continue 47 | self.n_items = max(self.n_items, max(items)) 48 | self.n_test += len(items) 49 | self.n_items += 1 50 | self.n_users += 1 51 | 52 | self.print_statistics() 53 | 54 | self.R = sp.dok_matrix((self.n_users, self.n_items), dtype=np.float32) 55 | 56 | self.train_items, self.test_set = {}, {} 57 | with open(train_file) as f_train: 58 | with open(test_file) as f_test: 59 | for l in f_train.readlines(): 60 | if len(l) == 0: break 61 | l = l.strip('\n') 62 | items = [int(i) for i in l.split(' ')] 63 | uid, train_items = items[0], items[1:] 64 | 65 | for i in train_items: 66 | self.R[uid, i] = 1. 67 | # self.R[uid][i] = 1 68 | 69 | self.train_items[uid] = train_items 70 | 71 | for l in f_test.readlines(): 72 | if len(l) == 0: break 73 | l = l.strip('\n') 74 | try: 75 | items = [int(i) for i in l.split(' ')] 76 | except Exception: 77 | continue 78 | 79 | uid, test_items = items[0], items[1:] 80 | self.test_set[uid] = test_items 81 | 82 | def get_adj_mat(self): 83 | try: 84 | t1 = time() 85 | adj_mat = sp.load_npz(self.path + '/s_adj_mat.npz') 86 | norm_adj_mat = sp.load_npz(self.path + '/s_norm_adj_mat.npz') 87 | mean_adj_mat = sp.load_npz(self.path + '/s_mean_adj_mat.npz') 88 | print('already load adj matrix', adj_mat.shape, time() - t1) 89 | 90 | except Exception: 91 | adj_mat, norm_adj_mat, mean_adj_mat = self.create_adj_mat() 92 | sp.save_npz(self.path + '/s_adj_mat.npz', adj_mat) 93 | sp.save_npz(self.path + '/s_norm_adj_mat.npz', norm_adj_mat) 94 | sp.save_npz(self.path + '/s_mean_adj_mat.npz', mean_adj_mat) 95 | return adj_mat, norm_adj_mat, mean_adj_mat 96 | 97 | def create_adj_mat(self): 98 | t1 = time() 99 | adj_mat = sp.dok_matrix((self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32) 100 | adj_mat = adj_mat.tolil() 101 | R = self.R.tolil() 102 | 103 | adj_mat[:self.n_users, self.n_users:] = R 104 | adj_mat[self.n_users:, :self.n_users] = R.T 105 | adj_mat = adj_mat.todok() 106 | print('already create adjacency matrix', adj_mat.shape, time() - t1) 107 | 108 | t2 = time() 109 | 110 | def normalized_adj_single(adj): 111 | rowsum = np.array(adj.sum(1)) 112 | 113 | d_inv = np.power(rowsum, -1).flatten() 114 | d_inv[np.isinf(d_inv)] = 0. 115 | d_mat_inv = sp.diags(d_inv) 116 | 117 | norm_adj = d_mat_inv.dot(adj) 118 | # norm_adj = adj.dot(d_mat_inv) 119 | print('generate single-normalized adjacency matrix.') 120 | return norm_adj.tocoo() 121 | 122 | def check_adj_if_equal(adj): 123 | dense_A = np.array(adj.todense()) 124 | degree = np.sum(dense_A, axis=1, keepdims=False) 125 | 126 | temp = np.dot(np.diag(np.power(degree, -1)), dense_A) 127 | print('check normalized adjacency matrix whether equal to this laplacian matrix.') 128 | return temp 129 | 130 | norm_adj_mat = normalized_adj_single(adj_mat + sp.eye(adj_mat.shape[0])) 131 | mean_adj_mat = normalized_adj_single(adj_mat) 132 | 133 | print('already normalize adjacency matrix', time() - t2) 134 | return adj_mat.tocsr(), norm_adj_mat.tocsr(), mean_adj_mat.tocsr() 135 | 136 | def negative_pool(self): 137 | t1 = time() 138 | for u in self.train_items.keys(): 139 | neg_items = list(set(range(self.n_items)) - set(self.train_items[u])) 140 | pools = [rd.choice(neg_items) for _ in range(100)] 141 | self.neg_pools[u] = pools 142 | print('refresh negative pools', time() - t1) 143 | 144 | def sample(self): 145 | if self.batch_size <= self.n_users: 146 | users = rd.sample(self.exist_users, self.batch_size) 147 | else: 148 | users = [rd.choice(self.exist_users) for _ in range(self.batch_size)] 149 | 150 | 151 | def sample_pos_items_for_u(u, num): 152 | pos_items = self.train_items[u] 153 | n_pos_items = len(pos_items) 154 | pos_batch = [] 155 | while True: 156 | if len(pos_batch) == num: break 157 | pos_id = np.random.randint(low=0, high=n_pos_items, size=1)[0] 158 | pos_i_id = pos_items[pos_id] 159 | 160 | if pos_i_id not in pos_batch: 161 | pos_batch.append(pos_i_id) 162 | return pos_batch 163 | 164 | def sample_neg_items_for_u(u, num): 165 | neg_items = [] 166 | while True: 167 | if len(neg_items) == num: break 168 | neg_id = np.random.randint(low=0, high=self.n_items,size=1)[0] 169 | if neg_id not in self.train_items[u] and neg_id not in neg_items: 170 | neg_items.append(neg_id) 171 | return neg_items 172 | 173 | def sample_neg_items_for_u_from_pools(u, num): 174 | neg_items = list(set(self.neg_pools[u]) - set(self.train_items[u])) 175 | return rd.sample(neg_items, num) 176 | 177 | pos_items, neg_items = [], [] 178 | for u in users: 179 | pos_items += sample_pos_items_for_u(u, 1) 180 | neg_items += sample_neg_items_for_u(u, 1) 181 | 182 | return users, pos_items, neg_items 183 | 184 | def get_num_users_items(self): 185 | return self.n_users, self.n_items 186 | 187 | def print_statistics(self): 188 | print('n_users=%d, n_items=%d' % (self.n_users, self.n_items)) 189 | print('n_interactions=%d' % (self.n_train + self.n_test)) 190 | print('n_train=%d, n_test=%d, sparsity=%.5f' % (self.n_train, self.n_test, (self.n_train + self.n_test)/(self.n_users * self.n_items))) 191 | 192 | 193 | def get_sparsity_split(self): 194 | try: 195 | split_uids, split_state = [], [] 196 | lines = open(self.path + '/sparsity.split', 'r').readlines() 197 | 198 | for idx, line in enumerate(lines): 199 | if idx % 2 == 0: 200 | split_state.append(line.strip()) 201 | print(line.strip()) 202 | else: 203 | split_uids.append([int(uid) for uid in line.strip().split(' ')]) 204 | print('get sparsity split.') 205 | 206 | except Exception: 207 | split_uids, split_state = self.create_sparsity_split() 208 | f = open(self.path + '/sparsity.split', 'w') 209 | for idx in range(len(split_state)): 210 | f.write(split_state[idx] + '\n') 211 | f.write(' '.join([str(uid) for uid in split_uids[idx]]) + '\n') 212 | print('create sparsity split.') 213 | 214 | return split_uids, split_state 215 | 216 | 217 | 218 | def create_sparsity_split(self): 219 | all_users_to_test = list(self.test_set.keys()) 220 | user_n_iid = dict() 221 | 222 | # generate a dictionary to store (key=n_iids, value=a list of uid). 223 | for uid in all_users_to_test: 224 | train_iids = self.train_items[uid] 225 | test_iids = self.test_set[uid] 226 | 227 | n_iids = len(train_iids) + len(test_iids) 228 | 229 | if n_iids not in user_n_iid.keys(): 230 | user_n_iid[n_iids] = [uid] 231 | else: 232 | user_n_iid[n_iids].append(uid) 233 | split_uids = list() 234 | 235 | # split the whole user set into four subset. 236 | temp = [] 237 | count = 1 238 | fold = 4 239 | n_count = (self.n_train + self.n_test) 240 | n_rates = 0 241 | 242 | split_state = [] 243 | for idx, n_iids in enumerate(sorted(user_n_iid)): 244 | temp += user_n_iid[n_iids] 245 | n_rates += n_iids * len(user_n_iid[n_iids]) 246 | n_count -= n_iids * len(user_n_iid[n_iids]) 247 | 248 | if n_rates >= count * 0.25 * (self.n_train + self.n_test): 249 | split_uids.append(temp) 250 | 251 | state = '#inter per user<=[%d], #users=[%d], #all rates=[%d]' %(n_iids, len(temp), n_rates) 252 | split_state.append(state) 253 | print(state) 254 | 255 | temp = [] 256 | n_rates = 0 257 | fold -= 1 258 | 259 | if idx == len(user_n_iid.keys()) - 1 or n_count == 0: 260 | split_uids.append(temp) 261 | 262 | state = '#inter per user<=[%d], #users=[%d], #all rates=[%d]' % (n_iids, len(temp), n_rates) 263 | split_state.append(state) 264 | print(state) 265 | 266 | 267 | 268 | return split_uids, split_state 269 | -------------------------------------------------------------------------------- /NGCF/utility/metrics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.metrics import roc_auc_score 3 | 4 | def recall(rank, ground_truth, N): 5 | return len(set(rank[:N]) & set(ground_truth)) / float(len(set(ground_truth))) 6 | 7 | 8 | def precision_at_k(r, k): 9 | """Score is precision @ k 10 | Relevance is binary (nonzero is relevant). 11 | Returns: 12 | Precision @ k 13 | Raises: 14 | ValueError: len(r) must be >= k 15 | """ 16 | assert k >= 1 17 | r = np.asarray(r)[:k] 18 | return np.mean(r) 19 | 20 | 21 | def average_precision(r,cut): 22 | """Score is average precision (area under PR curve) 23 | Relevance is binary (nonzero is relevant). 24 | Returns: 25 | Average precision 26 | """ 27 | r = np.asarray(r) 28 | out = [precision_at_k(r, k + 1) for k in range(cut) if r[k]] 29 | if not out: 30 | return 0. 31 | return np.sum(out)/float(min(cut, np.sum(r))) 32 | 33 | 34 | def mean_average_precision(rs): 35 | """Score is mean average precision 36 | Relevance is binary (nonzero is relevant). 37 | Returns: 38 | Mean average precision 39 | """ 40 | return np.mean([average_precision(r) for r in rs]) 41 | 42 | 43 | def dcg_at_k(r, k, method=1): 44 | """Score is discounted cumulative gain (dcg) 45 | Relevance is positive real values. Can use binary 46 | as the previous methods. 47 | Returns: 48 | Discounted cumulative gain 49 | """ 50 | r = np.asfarray(r)[:k] 51 | if r.size: 52 | if method == 0: 53 | return r[0] + np.sum(r[1:] / np.log2(np.arange(2, r.size + 1))) 54 | elif method == 1: 55 | return np.sum(r / np.log2(np.arange(2, r.size + 2))) 56 | else: 57 | raise ValueError('method must be 0 or 1.') 58 | return 0. 59 | 60 | 61 | def ndcg_at_k(r, k, method=1): 62 | """Score is normalized discounted cumulative gain (ndcg) 63 | Relevance is positive real values. Can use binary 64 | as the previous methods. 65 | Returns: 66 | Normalized discounted cumulative gain 67 | """ 68 | dcg_max = dcg_at_k(sorted(r, reverse=True), k, method) 69 | if not dcg_max: 70 | return 0. 71 | return dcg_at_k(r, k, method) / dcg_max 72 | 73 | 74 | def recall_at_k(r, k, all_pos_num): 75 | r = np.asfarray(r)[:k] 76 | return np.sum(r) / all_pos_num 77 | 78 | 79 | def hit_at_k(r, k): 80 | r = np.array(r)[:k] 81 | if np.sum(r) > 0: 82 | return 1. 83 | else: 84 | return 0. 85 | 86 | def F1(pre, rec): 87 | if pre + rec > 0: 88 | return (2.0 * pre * rec) / (pre + rec) 89 | else: 90 | return 0. 91 | 92 | def auc(ground_truth, prediction): 93 | try: 94 | res = roc_auc_score(y_true=ground_truth, y_score=prediction) 95 | except Exception: 96 | res = 0. 97 | return res -------------------------------------------------------------------------------- /NGCF/utility/parser.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Oct 10, 2018 3 | Tensorflow Implementation of Neural Graph Collaborative Filtering (NGCF) model in: 4 | Wang Xiang et al. Neural Graph Collaborative Filtering. In SIGIR 2019. 5 | 6 | @author: Xiang Wang (xiangwang@u.nus.edu) 7 | ''' 8 | import argparse 9 | 10 | def parse_args(): 11 | parser = argparse.ArgumentParser(description="Run NGCF.") 12 | parser.add_argument('--weights_path', nargs='?', default='', 13 | help='Store model path.') 14 | parser.add_argument('--data_path', nargs='?', default='../Data/', 15 | help='Input data path.') 16 | parser.add_argument('--proj_path', nargs='?', default='', 17 | help='Project path.') 18 | 19 | parser.add_argument('--dataset', nargs='?', default='gowalla', 20 | help='Choose a dataset from {gowalla, yelp2018, amazon-book}') 21 | parser.add_argument('--pretrain', type=int, default=0, 22 | help='0: No pretrain, -1: Pretrain with the learned embeddings, 1:Pretrain with stored models.') 23 | parser.add_argument('--verbose', type=int, default=1, 24 | help='Interval of evaluation.') 25 | parser.add_argument('--epoch', type=int, default=500, 26 | help='Number of epoch.') 27 | 28 | parser.add_argument('--embed_size', type=int, default=64, 29 | help='Embedding size.') 30 | parser.add_argument('--layer_size', nargs='?', default='[64]', 31 | help='Output sizes of every layer') 32 | parser.add_argument('--batch_size', type=int, default=1024, 33 | help='Batch size.') 34 | 35 | parser.add_argument('--regs', nargs='?', default='[1e-5,1e-5,1e-2]', 36 | help='Regularizations.') 37 | parser.add_argument('--lr', type=float, default=0.01, 38 | help='Learning rate.') 39 | 40 | parser.add_argument('--model_type', nargs='?', default='ngcf', 41 | help='Specify the name of model (ngcf).') 42 | parser.add_argument('--adj_type', nargs='?', default='norm', 43 | help='Specify the type of the adjacency (laplacian) matrix from {plain, norm, mean}.') 44 | parser.add_argument('--alg_type', nargs='?', default='ngcf', 45 | help='Specify the type of the graph convolutional layer from {ngcf, gcn, gcmc}.') 46 | 47 | parser.add_argument('--gpu_id', type=int, default=0, 48 | help='0 for NAIS_prod, 1 for NAIS_concat') 49 | 50 | parser.add_argument('--node_dropout_flag', type=int, default=0, 51 | help='0: Disable node dropout, 1: Activate node dropout') 52 | parser.add_argument('--node_dropout', nargs='?', default='[0.1]', 53 | help='Keep probability w.r.t. node dropout (i.e., 1-dropout_ratio) for each deep layer. 1: no dropout.') 54 | parser.add_argument('--mess_dropout', nargs='?', default='[0.1]', 55 | help='Keep probability w.r.t. message dropout (i.e., 1-dropout_ratio) for each deep layer. 1: no dropout.') 56 | 57 | parser.add_argument('--Ks', nargs='?', default='[20, 40, 60, 80, 100]', 58 | help='Output sizes of every layer') 59 | 60 | parser.add_argument('--save_flag', type=int, default=0, 61 | help='0: Disable model saver, 1: Activate model saver') 62 | 63 | parser.add_argument('--test_flag', nargs='?', default='part', 64 | help='Specify the test type from {part, full}, indicating whether the reference is done in mini-batch') 65 | 66 | parser.add_argument('--report', type=int, default=0, 67 | help='0: Disable performance report w.r.t. sparsity levels, 1: Show performance report w.r.t. sparsity levels') 68 | return parser.parse_args() 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neural Graph Collaborative Filtering 2 | This is our Tensorflow implementation for the paper: 3 | 4 | >Xiang Wang, Xiangnan He, Meng Wang, Fuli Feng, and Tat-Seng Chua (2019). Neural Graph Collaborative Filtering, [Paper in ACM DL](https://dl.acm.org/citation.cfm?doid=3331184.3331267) or [Paper in arXiv](https://arxiv.org/abs/1905.08108). In SIGIR'19, Paris, France, July 21-25, 2019. 5 | 6 | Author: Dr. Xiang Wang (xiangwang at u.nus.edu) 7 | 8 | ## Introduction 9 | Neural Graph Collaborative Filtering (NGCF) is a new recommendation framework based on graph neural network, explicitly encoding the collaborative signal in the form of high-order connectivities in user-item bipartite graph by performing embedding propagation. 10 | 11 | ## Citation 12 | If you want to use our codes and datasets in your research, please cite: 13 | ``` 14 | @inproceedings{NGCF19, 15 | author = {Xiang Wang and 16 | Xiangnan He and 17 | Meng Wang and 18 | Fuli Feng and 19 | Tat{-}Seng Chua}, 20 | title = {Neural Graph Collaborative Filtering}, 21 | booktitle = {Proceedings of the 42nd International {ACM} {SIGIR} Conference on 22 | Research and Development in Information Retrieval, {SIGIR} 2019, Paris, 23 | France, July 21-25, 2019.}, 24 | pages = {165--174}, 25 | year = {2019}, 26 | } 27 | ``` 28 | ## Environment Requirement 29 | The code has been tested running under Python 3.6.5. The required packages are as follows: 30 | * tensorflow == 1.8.0 31 | * numpy == 1.14.3 32 | * scipy == 1.1.0 33 | * sklearn == 0.19.1 34 | 35 | ## Example to Run the Codes 36 | The instruction of commands has been clearly stated in the codes (see the parser function in NGCF/utility/parser.py). 37 | * Gowalla dataset 38 | ``` 39 | python NGCF.py --dataset gowalla --regs [1e-5] --embed_size 64 --layer_size [64,64,64] --lr 0.0001 --save_flag 1 --pretrain 0 --batch_size 1024 --epoch 400 --verbose 1 --node_dropout [0.1] --mess_dropout [0.1,0.1,0.1] 40 | ``` 41 | 42 | * Amazon-book dataset 43 | ``` 44 | python NGCF.py --dataset amazon-book --regs [1e-5] --embed_size 64 --layer_size [64,64,64] --lr 0.0005 --save_flag 1 --pretrain 0 --batch_size 1024 --epoch 200 --verbose 50 --node_dropout [0.1] --mess_dropout [0.1,0.1,0.1] 45 | ``` 46 | Some important arguments: 47 | * `alg_type` 48 | * It specifies the type of graph convolutional layer. 49 | * Here we provide three options: 50 | * `ngcf` (by default), proposed in [Neural Graph Collaborative Filtering](https://www.comp.nus.edu.sg/~xiangnan/papers/sigir19-NGCF.pdf), SIGIR2019. Usage: `--alg_type ngcf`. 51 | * `gcn`, proposed in [Semi-Supervised Classification with Graph Convolutional Networks](https://openreview.net/pdf?id=SJU4ayYgl), ICLR2018. Usage: `--alg_type gcn`. 52 | * `gcmc`, propsed in [Graph Convolutional Matrix Completion](https://www.kdd.org/kdd2018/files/deep-learning-day/DLDay18_paper_32.pdf), KDD2018. Usage: `--alg_type gcmc`. 53 | 54 | * `adj_type` 55 | * It specifies the type of laplacian matrix where each entry defines the decay factor between two connected nodes. 56 | * Here we provide four options: 57 | * `ngcf` (by default), where each decay factor between two connected nodes is set as 1(out degree of the node), while each node is also assigned with 1 for self-connections. Usage: `--adj_type ngcf`. 58 | * `plain`, where each decay factor between two connected nodes is set as 1. No self-connections are considered. Usage: `--adj_type plain`. 59 | * `norm`, where each decay factor bewteen two connected nodes is set as 1/(out degree of the node + self-conncetion). Usage: `--adj_type norm`. 60 | * `gcmc`, where each decay factor between two connected nodes is set as 1/(out degree of the node). No self-connections are considered. Usage: `--adj_type gcmc`. 61 | 62 | * `node_dropout` 63 | * It indicates the node dropout ratio, which randomly blocks a particular node and discard all its outgoing messages. Usage: `--node_dropout [0.1] --node_dropout_flag 1` 64 | * Note that the arguement `node_dropout_flag` also needs to be set as 1, since the node dropout could lead to higher computational cost compared to message dropout. 65 | 66 | * `mess_dropout` 67 | * It indicates the message dropout ratio, which randomly drops out the outgoing messages. Usage `--mess_dropout [0.1,0.1,0.1]`. 68 | 69 | ## Dataset 70 | We provide two processed datasets: Gowalla and Amazon-book. 71 | * `train.txt` 72 | * Train file. 73 | * Each line is a user with her/his positive interactions with items: userID\t a list of itemID\n. 74 | 75 | * `test.txt` 76 | * Test file (positive instances). 77 | * Each line is a user with her/his positive interactions with items: userID\t a list of itemID\n. 78 | * Note that here we treat all unobserved interactions as the negative instances when reporting performance. 79 | 80 | * `user_list.txt` 81 | * User file. 82 | * Each line is a triplet (org_id, remap_id) for one user, where org_id and remap_id represent the ID of the user in the original and our datasets, respectively. 83 | 84 | * `item_list.txt` 85 | * Item file. 86 | * Each line is a triplet (org_id, remap_id) for one item, where org_id and remap_id represent the ID of the item in the original and our datasets, respectively. 87 | 88 | ## Acknowledgement 89 | 90 | This research is supported by the National Research Foundation, Singapore under its International Research Centres in Singapore Funding Initiative. Any opinions, findings and conclusions or recommendations expressed in this material are those of the author(s) and do not reflect the views of National Research Foundation, Singapore. 91 | --------------------------------------------------------------------------------