[docs]classGCMC(GeneralRecommender):r"""GCMC is a model that incorporate graph autoencoders for recommendation. Graph autoencoders are comprised of: 1) a graph encoder model :math:`Z = f(X; A)`, which take as input an :math:`N \times D` feature matrix X and a graph adjacency matrix A, and produce an :math:`N \times E` node embedding matrix :math:`Z = [z_1^T,..., z_N^T ]^T`; 2) a pairwise decoder model :math:`\hat A = g(Z)`, which takes pairs of node embeddings :math:`(z_i, z_j)` and predicts respective entries :math:`\hat A_{ij}` in the adjacency matrix. Note that :math:`N` denotes the number of nodes, :math:`D` the number of input features, and :math:`E` the embedding size. We implement the model following the original author with a pairwise training mode. """input_type=InputType.PAIRWISEdef__init__(self,config,dataset):super(GCMC,self).__init__(config,dataset)# load dataset infoself.num_all=self.n_users+self.n_itemsself.interaction_matrix=dataset.inter_matrix(form='coo').astype(np.float32)# csr# load parameters infoself.dropout_prob=config['dropout_prob']self.sparse_feature=config['sparse_feature']self.gcn_output_dim=config['gcn_output_dim']self.dense_output_dim=config['embedding_size']self.n_class=config['class_num']self.num_basis_functions=config['num_basis_functions']# generate node featureifself.sparse_feature:features=self.get_sparse_eye_mat(self.num_all)i=features._indices()v=features._values()self.user_features=torch.sparse.FloatTensor(i[:,:self.n_users],v[:self.n_users],torch.Size([self.n_users,self.num_all])).to(self.device)item_i=i[:,self.n_users:]item_i[0,:]=item_i[0,:]-self.n_usersself.item_features=torch.sparse.FloatTensor(item_i,v[self.n_users:],torch.Size([self.n_items,self.num_all])).to(self.device)else:features=torch.eye(self.num_all).to(self.device)self.user_features,self.item_features=torch.split(features,[self.n_users,self.n_items])self.input_dim=self.user_features.shape[1]# adj matrices for each relation are stored in self.supportself.Graph=self.get_norm_adj_mat().to(self.device)self.support=[self.Graph]# accumulation operationself.accum=config['accum']ifself.accum=='stack':div=self.gcn_output_dim//len(self.support)ifself.gcn_output_dim%len(self.support)!=0:self.logger.warning("HIDDEN[0] (=%d) of stack layer is adjusted to %d (in %d splits)."%(self.gcn_output_dim,len(self.support)*div,len(self.support)))self.gcn_output_dim=len(self.support)*div# define layers and lossself.GcEncoder=GcEncoder(accum=self.accum,num_user=self.n_users,num_item=self.n_items,support=self.support,input_dim=self.input_dim,gcn_output_dim=self.gcn_output_dim,dense_output_dim=self.dense_output_dim,drop_prob=self.dropout_prob,device=self.device,sparse_feature=self.sparse_feature).to(self.device)self.BiDecoder=BiDecoder(input_dim=self.dense_output_dim,output_dim=self.n_class,drop_prob=0.,device=self.device,num_weights=self.num_basis_functions).to(self.device)self.loss_function=nn.CrossEntropyLoss()
[docs]defget_sparse_eye_mat(self,num):r"""Get the normalized sparse eye matrix. Construct the sparse eye matrix as node feature. Args: num: the number of rows Returns: Sparse tensor of the normalized interaction matrix. """i=torch.LongTensor([range(0,num),range(0,num)])val=torch.FloatTensor([1]*num)returntorch.sparse.FloatTensor(i,val)
[docs]defget_norm_adj_mat(self):r"""Get the normalized interaction matrix of users and items. Construct the square matrix from the training data and normalize it using the laplace matrix. .. math:: A_{hat} = D^{-0.5} \times A \times D^{-0.5} Returns: Sparse tensor of the normalized interaction matrix. """# build adj matrixA=sp.dok_matrix((self.n_users+self.n_items,self.n_users+self.n_items),dtype=np.float32)inter_M=self.interaction_matrixinter_M_t=self.interaction_matrix.transpose()data_dict=dict(zip(zip(inter_M.row,inter_M.col+self.n_users),[1]*inter_M.nnz))data_dict.update(dict(zip(zip(inter_M_t.row+self.n_users,inter_M_t.col),[1]*inter_M_t.nnz)))A._update(data_dict)# norm adj matrixsumArr=(A>0).sum(axis=1)# add epsilon to avoid divide by zero Warningdiag=np.array(sumArr.flatten())[0]+1e-7diag=np.power(diag,-0.5)D=sp.diags(diag)L=D*A*D# covert norm_adj matrix to tensorL=sp.coo_matrix(L)row=L.rowcol=L.coli=torch.LongTensor([row,col])data=torch.FloatTensor(L.data)SparseL=torch.sparse.FloatTensor(i,data,torch.Size(L.shape))returnSparseL
[docs]defforward(self,user_X,item_X,user,item):# Graph autoencoders are comprised of a graph encoder model and a pairwise decoder model.user_embedding,item_embedding=self.GcEncoder(user_X,item_X)predict_score=self.BiDecoder(user_embedding,item_embedding,user,item)returnpredict_score
[docs]classGcEncoder(nn.Module):r"""Graph Convolutional Encoder GcEncoder take as input an :math:`N \times D` feature matrix :math:`X` and a graph adjacency matrix :math:`A`, and produce an :math:`N \times E` node embedding matrix; Note that :math:`N` denotes the number of nodes, :math:`D` the number of input features, and :math:`E` the embedding size. """def__init__(self,accum,num_user,num_item,support,input_dim,gcn_output_dim,dense_output_dim,drop_prob,device,sparse_feature=True,act_dense=lambdax:x,share_user_item_weights=True,bias=False):super(GcEncoder,self).__init__()self.num_users=num_userself.num_items=num_itemself.input_dim=input_dimself.gcn_output_dim=gcn_output_dimself.dense_output_dim=dense_output_dimself.accum=accumself.sparse_feature=sparse_featureself.device=deviceself.dropout_prob=drop_probself.dropout=nn.Dropout(p=self.dropout_prob)ifself.sparse_feature:self.sparse_dropout=SparseDropout(p=self.dropout_prob)else:self.sparse_dropout=nn.Dropout(p=self.dropout_prob)self.dense_activate=act_denseself.activate=nn.ReLU()self.share_weights=share_user_item_weightsself.bias=biasself.support=supportself.num_support=len(support)# gcn layerifself.accum=='sum':self.weights_u=nn.ParameterList([nn.Parameter(torch.FloatTensor(self.input_dim,self.gcn_output_dim).to(self.device),requires_grad=True)for_inrange(self.num_support)])ifshare_user_item_weights:self.weights_v=self.weights_uelse:self.weights_v=nn.ParameterList([nn.Parameter(torch.FloatTensor(self.input_dim,self.gcn_output_dim).to(self.device),requires_grad=True)for_inrange(self.num_support)])else:assertself.gcn_output_dim%self.num_support==0,'output_dim must be multiple of num_support for stackGC'self.sub_hidden_dim=self.gcn_output_dim//self.num_supportself.weights_u=nn.ParameterList([nn.Parameter(torch.FloatTensor(self.input_dim,self.sub_hidden_dim).to(self.device),requires_grad=True)for_inrange(self.num_support)])ifshare_user_item_weights:self.weights_v=self.weights_uelse:self.weights_v=nn.ParameterList([nn.Parameter(torch.FloatTensor(self.input_dim,self.sub_hidden_dim).to(self.device),requires_grad=True)for_inrange(self.num_support)])# dense layerself.dense_layer_u=nn.Linear(self.gcn_output_dim,self.dense_output_dim,bias=self.bias)ifshare_user_item_weights:self.dense_layer_v=self.dense_layer_uelse:self.dense_layer_v=nn.Linear(self.gcn_output_dim,self.dense_output_dim,bias=self.bias)self._init_weights()def_init_weights(self):init_range=math.sqrt((self.num_support+1)/(self.input_dim+self.gcn_output_dim))forwinrange(self.num_support):self.weights_u[w].data.uniform_(-init_range,init_range)ifnotself.share_weights:forwinrange(self.num_support):self.weights_v[w].data.uniform_(-init_range,init_range)dense_init_range=math.sqrt((self.num_support+1)/(self.dense_output_dim+self.gcn_output_dim))self.dense_layer_u.weight.data.uniform_(-dense_init_range,dense_init_range)ifnotself.share_weights:self.dense_layer_v.weight.data.uniform_(-dense_init_range,dense_init_range)ifself.bias:self.dense_layer_u.bias.data.fill_(0)ifnotself.share_weights:self.dense_layer_v.bias.data.fill_(0)
[docs]defforward(self,user_X,item_X):# ----------------------------------------GCN layer----------------------------------------user_X=self.sparse_dropout(user_X)item_X=self.sparse_dropout(item_X)embeddings=[]ifself.accum=='sum':wu=0.wv=0.foriinrange(self.num_support):# weight sharingwu=self.weights_u[i]+wuwv=self.weights_v[i]+wv# multiply feature matrices with weightsifself.sparse_feature:temp_u=torch.sparse.mm(user_X,wu)temp_v=torch.sparse.mm(item_X,wv)else:temp_u=torch.mm(user_X,wu)temp_v=torch.mm(item_X,wv)all_embedding=torch.cat([temp_u,temp_v])# then multiply with adj matricesgraph_A=self.support[i]all_emb=torch.sparse.mm(graph_A,all_embedding)embeddings.append(all_emb)embeddings=torch.stack(embeddings,dim=1)embeddings=torch.sum(embeddings,dim=1)else:foriinrange(self.num_support):# multiply feature matrices with weightsifself.sparse_feature:temp_u=torch.sparse.mm(user_X,self.weights_u[i])temp_v=torch.sparse.mm(item_X,self.weights_v[i])else:temp_u=torch.mm(user_X,self.weights_u[i])temp_v=torch.mm(item_X,self.weights_v[i])all_embedding=torch.cat([temp_u,temp_v])# then multiply with adj matricesgraph_A=self.support[i]all_emb=torch.sparse.mm(graph_A,all_embedding)embeddings.append(all_emb)embeddings=torch.cat(embeddings,dim=1)users,items=torch.split(embeddings,[self.num_users,self.num_items])u_hidden=self.activate(users)v_hidden=self.activate(items)# ----------------------------------------Dense Layer----------------------------------------u_hidden=self.dropout(u_hidden)v_hidden=self.dropout(v_hidden)u_hidden=self.dense_layer_u(u_hidden)v_hidden=self.dense_layer_u(v_hidden)u_outputs=self.dense_activate(u_hidden)v_outputs=self.dense_activate(v_hidden)returnu_outputs,v_outputs
[docs]classBiDecoder(nn.Module):"""Bi-linear decoder BiDecoder takes pairs of node embeddings and predicts respective entries in the adjacency matrix. """def__init__(self,input_dim,output_dim,drop_prob,device,num_weights=3,act=lambdax:x):super(BiDecoder,self).__init__()self.input_dim=input_dimself.output_dim=output_dimself.num_weights=num_weightsself.device=deviceself.activate=actself.dropout_prob=drop_probself.dropout=nn.Dropout(p=self.dropout_prob)self.weights=nn.ParameterList([nn.Parameter(orthogonal([self.input_dim,self.input_dim]).to(self.device))for_inrange(self.num_weights)])self.dense_layer=nn.Linear(self.num_weights,self.output_dim,bias=False)self._init_weights()def_init_weights(self):dense_init_range=math.sqrt(self.output_dim/(self.num_weights+self.output_dim))self.dense_layer.weight.data.uniform_(-dense_init_range,dense_init_range)
[docs]deforthogonal(shape,scale=1.1):""" Initialization function for weights in class GCMC. From Lasagne. Reference: Saxe et al., http://arxiv.org/abs/1312.6120 """flat_shape=(shape[0],np.prod(shape[1:]))a=np.random.normal(0.0,1.0,flat_shape)u,_,v=np.linalg.svd(a,full_matrices=False)# pick the one with the correct shapeq=uifu.shape==flat_shapeelsevq=q.reshape(shape)returntorch.tensor(scale*q[:shape[0],:shape[1]],dtype=torch.float32)