├── README.md ├── layers.py ├── train.py └── utils.py /README.md: -------------------------------------------------------------------------------- 1 | # Graph Convolutional Networks for relational graphs 2 | pytorch-based implementation of Relational Graph Convolutional Networks for semi-supervised node classification on (directed) relational graphs. 3 | it is pytorch version of [https://github.com/tkipf/relational-gcn](https://github.com/tkipf/relational-gcn) 4 | 5 | 6 | 7 | ## Dependencies 8 | 9 | 10 | * Pytorch (0.4.1) 11 | * Python (3.6.3) 12 | * scipy (0.19.1) 13 | * numpy (1.14.5) 14 | 15 | 16 | ## References 17 | 18 | [1] M. Schlichtkrull, T. N. Kipf, P. Bloem, R. van den Berg, I. Titov, M. Welling, [Modeling Relational Data with Graph Convolutional Networks](https://arxiv.org/abs/1703.06103), 2017 19 | 20 | 21 | ## Cite 22 | 23 | ``` 24 | @article{schlichtkrull2017modeling, 25 | title={Modeling Relational Data with Graph Convolutional Networks}, 26 | author={Schlichtkrull, Michael and Kipf, Thomas N and Bloem, Peter and Berg, Rianne van den and Titov, Ivan and Welling, Max}, 27 | journal={arXiv preprint arXiv:1703.06103}, 28 | year={2017} 29 | } 30 | -------------------------------------------------------------------------------- /layers.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.autograd as autograd 3 | import torch.nn as nn 4 | import torch.optim as optim 5 | import torch.nn.functional as F 6 | import pickle 7 | from tqdm import tqdm 8 | import numpy as np 9 | from scipy import sparse 10 | 11 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 12 | 13 | class GraphConvolution(nn.Module): 14 | def __init__(self, input_dim, output_dim, support=1, featureless=True, 15 | init='glorot_uniform', activation='linear', 16 | weights=None, W_regularizer=None, num_bases=-1, 17 | b_regularizer=None, bias=False, dropout=0., **kwargs): 18 | super(GraphConvolution, self).__init__() 19 | #self.init = initializations.get(init) 20 | #self.activation = activations.get(activation) 21 | if activation == "relu": 22 | self.activation = nn.ReLU() 23 | elif activation == "softmax": 24 | self.activation = nn.Softmax(dim=-1) 25 | else: 26 | self.activation = F.ReLU() 27 | self.input_dim = input_dim 28 | self.output_dim = output_dim # number of features per node 29 | self.support = support # filter support / number of weights 30 | self.featureless = featureless # use/ignore input features 31 | self.dropout = dropout 32 | self.w_regularizer = nn.L1Loss() 33 | 34 | assert support >= 1 35 | 36 | #TODO 37 | 38 | self.bias = bias 39 | self.initial_weights = weights 40 | self.num_bases = num_bases 41 | 42 | # these will be defined during build() 43 | #self.input_dim = None 44 | if self.num_bases > 0: 45 | self.W = nn.Parameter(torch.empty(self.input_dim * self.num_bases, self.output_dim, dtype=torch.float32, device=device)) 46 | self.W_comp = nn.Parameter(torch.empty(self.support, self.num_bases, dtype=torch.float32, device=device)) 47 | nn.init.xavier_uniform_(self.W_comp) 48 | else: 49 | self.W = nn.Parameter(torch.empty(self.input_dim * self.support, self.output_dim, dtype=torch.float32, device=device)) 50 | nn.init.xavier_uniform_(self.W) 51 | 52 | if self.bias: 53 | self.b = nn.Parameter(torch.empty(self.output_dim, dtype=torch.float32, device=device)) 54 | nn.init.xavier_uniform_(self.b) 55 | 56 | self.dropout = nn.Dropout(dropout) 57 | 58 | def get_output_shape_for(self, input_shapes): 59 | features_shape = input_shapes[0] 60 | output_shape = (features_shape[0], self.output_dim) 61 | return output_shape # (batch_size, output_dim) 62 | 63 | def forward(self, inputs, mask=None): 64 | features = torch.tensor(inputs[0], dtype=torch.float32, device=device) 65 | A = inputs[1:] # list of basis functions 66 | A = [torch.sparse.FloatTensor(torch.LongTensor(a.nonzero()) 67 | , torch.FloatTensor(sparse.find(a)[-1]) 68 | ,torch.Size(a.shape)).to(device) 69 | if len(sparse.find(a)[-1]) > 0 else torch.sparse.FloatTensor(a.shape[0],a.shape[1]) 70 | for a in A] 71 | # convolve 72 | if not self.featureless: 73 | supports = list() 74 | for i in range(self.support): 75 | supports.append(torch.spmm(A[i], features)) 76 | supports = torch.cat(supports, dim=1) 77 | else: 78 | values = torch.cat([i._values() for i in A], dim=-1) 79 | indices = torch.cat([torch.cat([j._indices()[0].reshape(1,-1), 80 | (j._indices()[1] + (i*self.input_dim)).reshape(1,-1)]) 81 | for i, j in enumerate(A)], dim=-1) 82 | supports = torch.sparse.FloatTensor(indices, values, torch.Size([A[0].shape[0], 83 | len(A)*self.input_dim])) 84 | self.num_nodes = supports.shape[0] 85 | if self.num_bases > 0: 86 | #self.W = self.W.reshape( 87 | # (self.num_bases, self.input_dim, self.output_dim)) 88 | #self.W = self.W.permute((1, 0, 2)) # (self.input_dim, self.num_bases, self.output_dim) 89 | V = torch.matmul(self.W_comp, self.W.reshape(self.num_bases, self.input_dim, self.output_dim).permute(1,0,2)) 90 | V = torch.reshape(V, (self.support*self.input_dim, self.output_dim)) 91 | output = torch.spmm(supports, V) 92 | else: 93 | output = torch.spmm(supports, self.W) 94 | 95 | # if featureless add dropout to output, by elementwise matmultiplying with column vector of ones, 96 | # with dropout applied to the vector of ones. 97 | if self.featureless: 98 | tmp = torch.ones(self.num_nodes) 99 | tmp_do = self.dropout(tmp) 100 | output = (output.transpose(1, 0) * tmp_do).transpose(1,0) 101 | 102 | if self.bias: 103 | output += self.b 104 | return self.activation(output) 105 | -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import time 3 | import sys 4 | import os 5 | 6 | import torch 7 | import torch.autograd as autograd 8 | import torch.nn as nn 9 | import torch.optim as optim 10 | import torch.nn.functional as F 11 | import pickle 12 | from tqdm import tqdm 13 | import numpy as np 14 | from collections import Counter 15 | from scipy import sparse 16 | from sklearn.metrics import accuracy_score 17 | 18 | from layers import * 19 | from utils import * 20 | 21 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 22 | np.random.seed() 23 | torch.manual_seed(0) 24 | 25 | ap = argparse.ArgumentParser() 26 | ap.add_argument("-d", "--dataset", type=str, default="aifb", 27 | help="Dataset string ('aifb', 'mutag', 'bgs', 'am')") 28 | ap.add_argument("-e", "--epochs", type=int, default=50, 29 | help="Number training epochs") 30 | ap.add_argument("-hd", "--hidden", type=int, default=16, 31 | help="Number hidden units") 32 | ap.add_argument("-do", "--dropout", type=float, default=0., 33 | help="Dropout rate") 34 | ap.add_argument("-b", "--bases", type=int, default=-1, 35 | help="Number of bases used (-1: all)") 36 | ap.add_argument("-lr", "--learnrate", type=float, default=0.01, 37 | help="Learning rate") 38 | ap.add_argument("-l2", "--l2norm", type=float, default=0., 39 | help="L2 normalization of input weights") 40 | fp = ap.add_mutually_exclusive_group(required=False) 41 | fp.add_argument('--validation', dest='validation', action='store_true') 42 | fp.add_argument('--testing', dest='validation', action='store_false') 43 | ap.set_defaults(validation=True) 44 | 45 | args = vars(ap.parse_args()) 46 | print(args) 47 | 48 | DATASET = args['dataset'] 49 | NB_EPOCH = args['epochs'] 50 | VALIDATION = args['validation'] 51 | LR = args['learnrate'] 52 | L2 = args['l2norm'] 53 | HIDDEN = args['hidden'] 54 | BASES = args['bases'] 55 | DO = args['dropout'] 56 | 57 | dirname = os.path.dirname(os.path.realpath(sys.argv[0])) 58 | 59 | with open(dirname + '/' + DATASET + '.pickle', 'rb') as f: 60 | data = pickle.load(f) 61 | 62 | A = data['A'] 63 | y = data['y'] 64 | train_idx = data['train_idx'] 65 | test_idx = data['test_idx'] 66 | del data 67 | 68 | for i in range(len(A)): 69 | d = np.array(A[i].sum(1)).flatten() 70 | d_inv = 1. / d 71 | d_inv[np.isinf(d_inv)] = 0. 72 | D_inv = sparse.diags(d_inv) 73 | A[i] = D_inv.dot(A[i]).tocsr() 74 | 75 | A = [i for i in A if len(i.nonzero()[0]) > 0] 76 | 77 | 78 | 79 | y_train, y_val, y_test, idx_train, idx_val, idx_test = get_splits(y, train_idx, 80 | test_idx, 81 | VALIDATION) 82 | output_dimension = y_train.shape[1] 83 | support = len(A) 84 | y_train = torch.tensor(y_train) 85 | y_val = torch.tensor(y_val) 86 | y_test = torch.tensor(y_test) 87 | 88 | 89 | class GraphClassifier(nn.Module): 90 | 91 | 92 | def __init__(self, input_dim, hidden_dim, output_dim, num_bases, dropout, support): 93 | super(GraphClassifier, self).__init__() 94 | self.gcn_1 = GraphConvolution(input_dim, hidden_dim, num_bases=num_bases, activation="relu", 95 | support=support) 96 | self.gcn_2 = GraphConvolution(hidden_dim, output_dim, num_bases=num_bases, activation="softmax", 97 | featureless=False, support=support) 98 | self.dropout = nn.Dropout(dropout) 99 | 100 | def forward(self, inputs, mask=None): 101 | output = self.gcn_1(inputs, mask=mask) 102 | output = self.dropout(output) 103 | output = self.gcn_2([output]+inputs[1:], mask=mask) 104 | return output 105 | 106 | if __name__ == "__main__": 107 | model = GraphClassifier(A[0].shape[0], HIDDEN, output_dimension, BASES, DO, len(A)) 108 | model.to(device) 109 | optimizer = optim.Adam(model.parameters(), lr=LR, weight_decay=L2) 110 | criterion = nn.CrossEntropyLoss() 111 | X = sparse.csr_matrix(A[0].shape).todense() 112 | for epoch in range(NB_EPOCH): 113 | t = time.time() 114 | output = model([X]+A) 115 | gold = y_train[idx_train].argmax(dim=-1) 116 | loss = criterion(output[idx_train], gold) 117 | score = accuracy_score(output[idx_train].argmax(dim=-1), gold) 118 | optimizer.zero_grad() 119 | loss.backward() 120 | optimizer.step() 121 | print("train_accuracy:",score,"loss:,",loss.item(), "time:", time.time() - t) 122 | test_gold = y_test[idx_test].argmax(dim=-1) 123 | test_output = output[idx_test] 124 | test_score = accuracy_score(test_output.argmax(dim=-1), test_gold) 125 | test_loss = criterion(test_output, test_gold) 126 | print("test_accuracy:", test_score, "loss:",test_loss.item()) 127 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def get_splits(y, train_idx, test_idx, validation=True): 4 | # Make dataset splits 5 | # np.random.shuffle(train_idx) 6 | if validation: 7 | idx_train = train_idx[len(train_idx) / 5:] 8 | idx_val = train_idx[:len(train_idx) / 5] 9 | idx_test = idx_val # report final score on validation set for hyperparameter optimization 10 | else: 11 | idx_train = train_idx 12 | idx_val = train_idx # no validation 13 | idx_test = test_idx 14 | 15 | y_train = np.zeros(y.shape) 16 | y_val = np.zeros(y.shape) 17 | y_test = np.zeros(y.shape) 18 | 19 | y_train[idx_train] = np.array(y[idx_train].todense()) 20 | y_val[idx_val] = np.array(y[idx_val].todense()) 21 | y_test[idx_test] = np.array(y[idx_test].todense()) 22 | 23 | return y_train, y_val, y_test, idx_train, idx_val, idx_test 24 | --------------------------------------------------------------------------------