dmlc--dgl
97b2ab53e2
* version-1 * version-2 * version-3 * update examples/README * Update .gitignore * update performance in README, delete scripts * 1st approving review * 2nd approving review Co-authored-by: Mufei Li <mufeili1996@gmail.com>
361 行
12 KiB
Python
361 行
12 KiB
Python
from ogb.graphproppred import Evaluator
|
|
import torch
|
|
import numpy as np
|
|
from dgl.dataloading import GraphDataLoader
|
|
from tqdm import tqdm
|
|
import dgl
|
|
import random
|
|
import torch.nn as nn
|
|
from ogb.graphproppred.mol_encoder import AtomEncoder
|
|
import torch.nn.functional as F
|
|
import torch.optim as optim
|
|
import argparse
|
|
from torch.utils.data import Dataset
|
|
from preprocessing import prepare_dataset
|
|
|
|
def aggregate_mean(h, vector_field, h_in):
|
|
return torch.mean(h, dim=1)
|
|
|
|
def aggregate_max(h, vector_field, h_in):
|
|
return torch.max(h, dim=1)[0]
|
|
|
|
def aggregate_sum(h, vector_field, h_in):
|
|
return torch.sum(h, dim=1)
|
|
|
|
def aggregate_dir_dx(h, vector_field, h_in, eig_idx=1):
|
|
eig_w = ((vector_field[:, :, eig_idx]) /
|
|
(torch.sum(torch.abs(vector_field[:, :, eig_idx]), keepdim=True, dim=1) + 1e-8)).unsqueeze(-1)
|
|
h_mod = torch.mul(h, eig_w)
|
|
return torch.abs(torch.sum(h_mod, dim=1) - torch.sum(eig_w, dim=1) * h_in)
|
|
|
|
class FCLayer(nn.Module):
|
|
def __init__(self, in_size, out_size):
|
|
super(FCLayer, self).__init__()
|
|
|
|
self.in_size = in_size
|
|
self.out_size = out_size
|
|
self.linear = nn.Linear(in_size, out_size, bias=True)
|
|
|
|
self.reset_parameters()
|
|
|
|
def reset_parameters(self):
|
|
nn.init.xavier_uniform_(self.linear.weight, 1 / self.in_size)
|
|
self.linear.bias.data.zero_()
|
|
|
|
def forward(self, x):
|
|
h = self.linear(x)
|
|
return h
|
|
|
|
class MLP(nn.Module):
|
|
def __init__(self, in_size, out_size):
|
|
super(MLP, self).__init__()
|
|
|
|
self.in_size = in_size
|
|
self.out_size = out_size
|
|
self.fc = FCLayer(in_size, out_size)
|
|
|
|
def forward(self, x):
|
|
x = self.fc(x)
|
|
return x
|
|
|
|
class DGNLayer(nn.Module):
|
|
def __init__(self, in_dim, out_dim, dropout, aggregators):
|
|
super().__init__()
|
|
|
|
self.dropout = dropout
|
|
|
|
self.aggregators = aggregators
|
|
|
|
self.batchnorm_h = nn.BatchNorm1d(out_dim)
|
|
self.pretrans = MLP(in_size=2 * in_dim, out_size=in_dim)
|
|
self.posttrans = MLP(in_size=(len(aggregators) * 1 + 1) * in_dim, out_size=out_dim)
|
|
|
|
def pretrans_edges(self, edges):
|
|
z2 = torch.cat([edges.src['h'], edges.dst['h']], dim=1)
|
|
vector_field = edges.data['eig']
|
|
return {'e': self.pretrans(z2), 'vector_field': vector_field}
|
|
|
|
def message_func(self, edges):
|
|
return {'e': edges.data['e'], 'vector_field': edges.data['vector_field']}
|
|
|
|
def reduce_func(self, nodes):
|
|
h_in = nodes.data['h']
|
|
h = nodes.mailbox['e']
|
|
|
|
vector_field = nodes.mailbox['vector_field']
|
|
|
|
h = torch.cat([aggregate(h, vector_field, h_in) for aggregate in self.aggregators], dim=1)
|
|
|
|
return {'h': h}
|
|
|
|
def forward(self, g, h, snorm_n):
|
|
|
|
g.ndata['h'] = h
|
|
|
|
# pretransformation
|
|
g.apply_edges(self.pretrans_edges)
|
|
|
|
# aggregation
|
|
g.update_all(self.message_func, self.reduce_func)
|
|
h = torch.cat([h, g.ndata['h']], dim=1)
|
|
|
|
# posttransformation
|
|
h = self.posttrans(h)
|
|
|
|
# graph and batch normalization
|
|
h = h * snorm_n
|
|
h = self.batchnorm_h(h)
|
|
h = F.relu(h)
|
|
|
|
h = F.dropout(h, self.dropout, training=self.training)
|
|
|
|
return h
|
|
|
|
class MLPReadout(nn.Module):
|
|
|
|
def __init__(self, input_dim, output_dim, L=2): # L=nb_hidden_layers
|
|
super().__init__()
|
|
list_FC_layers = [nn.Linear(input_dim // 2 ** l, input_dim // 2 ** (l + 1), bias=True) for l in range(L)]
|
|
list_FC_layers.append(nn.Linear(input_dim // 2 ** L, output_dim, bias=True))
|
|
self.FC_layers = nn.ModuleList(list_FC_layers)
|
|
self.L = L
|
|
|
|
def forward(self, x):
|
|
y = x
|
|
for l in range(self.L):
|
|
y = self.FC_layers[l](y)
|
|
y = F.relu(y)
|
|
y = self.FC_layers[self.L](y)
|
|
return y
|
|
|
|
class DGNNet(nn.Module):
|
|
def __init__(self, hidden_dim=420, out_dim=420, dropout=0.2, n_layers=4):
|
|
super().__init__()
|
|
|
|
self.embedding_h = AtomEncoder(emb_dim=hidden_dim)
|
|
self.aggregators = [aggregate_mean, aggregate_sum, aggregate_max, aggregate_dir_dx]
|
|
|
|
self.layers = nn.ModuleList([DGNLayer(in_dim=hidden_dim, out_dim=hidden_dim, dropout=dropout,
|
|
aggregators=self.aggregators) for _ in range(n_layers - 1)])
|
|
self.layers.append(DGNLayer(in_dim=hidden_dim, out_dim=out_dim, dropout=dropout,
|
|
aggregators=self.aggregators))
|
|
|
|
# 128 out dim since ogbg-molpcba has 128 tasks
|
|
self.MLP_layer = MLPReadout(out_dim, 128)
|
|
|
|
def forward(self, g, h, snorm_n):
|
|
h = self.embedding_h(h)
|
|
|
|
for i, conv in enumerate(self.layers):
|
|
h_t = conv(g, h, snorm_n)
|
|
h = h_t
|
|
|
|
g.ndata['h'] = h
|
|
|
|
hg = dgl.mean_nodes(g, 'h')
|
|
|
|
return self.MLP_layer(hg)
|
|
|
|
def loss(self, scores, labels):
|
|
is_labeled = labels == labels
|
|
loss = nn.BCEWithLogitsLoss()(scores[is_labeled], labels[is_labeled].float())
|
|
return loss
|
|
|
|
def train_epoch(model, optimizer, device, data_loader):
|
|
model.train()
|
|
epoch_loss = 0
|
|
epoch_train_AP = 0
|
|
list_scores = []
|
|
list_labels = []
|
|
for iter, (batch_graphs, batch_labels, batch_snorm_n) in enumerate(data_loader):
|
|
batch_graphs = batch_graphs.to(device)
|
|
batch_x = batch_graphs.ndata['feat'] # num x feat
|
|
batch_snorm_n = batch_snorm_n.to(device)
|
|
batch_labels = batch_labels.to(device)
|
|
optimizer.zero_grad()
|
|
|
|
batch_scores = model(batch_graphs, batch_x, batch_snorm_n)
|
|
|
|
loss = model.loss(batch_scores, batch_labels)
|
|
loss.backward()
|
|
optimizer.step()
|
|
epoch_loss += loss.item()
|
|
list_scores.append(batch_scores)
|
|
list_labels.append(batch_labels)
|
|
|
|
epoch_loss /= (iter + 1)
|
|
|
|
evaluator = Evaluator(name='ogbg-molpcba')
|
|
epoch_train_AP = evaluator.eval({'y_pred': torch.cat(list_scores),
|
|
'y_true': torch.cat(list_labels)})['ap']
|
|
|
|
return epoch_loss, epoch_train_AP
|
|
|
|
def evaluate_network(model, device, data_loader):
|
|
model.eval()
|
|
epoch_test_loss = 0
|
|
epoch_test_AP = 0
|
|
with torch.no_grad():
|
|
list_scores = []
|
|
list_labels = []
|
|
for iter, (batch_graphs, batch_labels, batch_snorm_n) in enumerate(data_loader):
|
|
batch_graphs = batch_graphs.to(device)
|
|
batch_x = batch_graphs.ndata['feat']
|
|
batch_snorm_n = batch_snorm_n.to(device)
|
|
batch_labels = batch_labels.to(device)
|
|
|
|
batch_scores = model(batch_graphs, batch_x, batch_snorm_n)
|
|
|
|
loss = model.loss(batch_scores, batch_labels)
|
|
epoch_test_loss += loss.item()
|
|
list_scores.append(batch_scores)
|
|
list_labels.append(batch_labels)
|
|
|
|
epoch_test_loss /= (iter + 1)
|
|
|
|
evaluator = Evaluator(name='ogbg-molpcba')
|
|
epoch_test_AP = evaluator.eval({'y_pred': torch.cat(list_scores),
|
|
'y_true': torch.cat(list_labels)})['ap']
|
|
|
|
return epoch_test_loss, epoch_test_AP
|
|
|
|
def train(dataset, params):
|
|
|
|
trainset, valset, testset = dataset.train, dataset.val, dataset.test
|
|
device = params.device
|
|
|
|
print("Training Graphs: ", len(trainset))
|
|
print("Validation Graphs: ", len(valset))
|
|
print("Test Graphs: ", len(testset))
|
|
|
|
model = DGNNet()
|
|
model = model.to(device)
|
|
|
|
# view model parameters
|
|
total_param = 0
|
|
print("MODEL DETAILS:\n")
|
|
for param in model.parameters():
|
|
total_param += np.prod(list(param.data.size()))
|
|
print('DGN Total parameters:', total_param)
|
|
|
|
optimizer = optim.Adam(model.parameters(), lr=0.0008, weight_decay=1e-5)
|
|
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min',
|
|
factor=0.8,
|
|
patience=8,
|
|
verbose=True)
|
|
|
|
epoch_train_losses, epoch_val_losses = [], []
|
|
epoch_train_APs, epoch_val_APs, epoch_test_APs = [], [], []
|
|
|
|
train_loader = GraphDataLoader(trainset, batch_size=params.batch_size, shuffle=True, collate_fn=dataset.collate, pin_memory=True)
|
|
val_loader = GraphDataLoader(valset, batch_size=params.batch_size, shuffle=False, collate_fn=dataset.collate, pin_memory=True)
|
|
test_loader = GraphDataLoader(testset, batch_size=params.batch_size, shuffle=False, collate_fn=dataset.collate, pin_memory=True)
|
|
|
|
with tqdm(range(450), unit='epoch') as t:
|
|
for epoch in t:
|
|
t.set_description('Epoch %d' % epoch)
|
|
|
|
epoch_train_loss, epoch_train_ap = train_epoch(model, optimizer, device, train_loader)
|
|
epoch_val_loss, epoch_val_ap = evaluate_network(model, device, val_loader)
|
|
|
|
epoch_train_losses.append(epoch_train_loss)
|
|
epoch_val_losses.append(epoch_val_loss)
|
|
epoch_train_APs.append(epoch_train_ap.item())
|
|
epoch_val_APs.append(epoch_val_ap.item())
|
|
|
|
_, epoch_test_ap = evaluate_network(model, device, test_loader)
|
|
|
|
epoch_test_APs.append(epoch_test_ap.item())
|
|
|
|
t.set_postfix(train_loss=epoch_train_loss,
|
|
train_AP=epoch_train_ap.item(), val_AP=epoch_val_ap.item(),
|
|
refresh=False)
|
|
|
|
scheduler.step(-epoch_val_ap.item())
|
|
|
|
if optimizer.param_groups[0]['lr'] < 1e-5:
|
|
print("\n!! LR EQUAL TO MIN LR SET.")
|
|
break
|
|
|
|
print('')
|
|
|
|
best_val_epoch = np.argmax(np.array(epoch_val_APs))
|
|
best_train_epoch = np.argmax(np.array(epoch_train_APs))
|
|
best_val_ap = epoch_val_APs[best_val_epoch]
|
|
best_val_test_ap = epoch_test_APs[best_val_epoch]
|
|
best_val_train_ap = epoch_train_APs[best_val_epoch]
|
|
best_train_ap = epoch_train_APs[best_train_epoch]
|
|
|
|
print("Best Train AP: {:.4f}".format(best_train_ap))
|
|
print("Best Val AP: {:.4f}".format(best_val_ap))
|
|
print("Test AP of Best Val: {:.4f}".format(best_val_test_ap))
|
|
print("Train AP of Best Val: {:.4f}".format(best_val_train_ap))
|
|
|
|
class Subset(object):
|
|
def __init__(self, dataset, labels, indices):
|
|
dataset = [dataset[idx] for idx in indices]
|
|
labels = [labels[idx] for idx in indices]
|
|
self.dataset, self.labels = [], []
|
|
for i, g in enumerate(dataset):
|
|
if g.num_nodes() > 5:
|
|
self.dataset.append(g)
|
|
self.labels.append(labels[i])
|
|
self.len = len(self.dataset)
|
|
|
|
def __getitem__(self, item):
|
|
return self.dataset[item], self.labels[item]
|
|
|
|
def __len__(self):
|
|
return self.len
|
|
|
|
class PCBADataset(Dataset):
|
|
def __init__(self, name):
|
|
print("[I] Loading dataset %s..." % (name))
|
|
self.name = name
|
|
|
|
self.dataset, self.split_idx = prepare_dataset(name)
|
|
print("One hot encoding substructure counts... ", end='')
|
|
self.d_id = [1]*self.dataset[0].edata['subgraph_counts'].shape[1]
|
|
|
|
for g in self.dataset:
|
|
g.edata['eig'] = g.edata['subgraph_counts'].float()
|
|
|
|
self.train = Subset(self.dataset, self.split_idx['label'], self.split_idx['train'])
|
|
self.val = Subset(self.dataset, self.split_idx['label'], self.split_idx['valid'])
|
|
self.test = Subset(self.dataset, self.split_idx['label'], self.split_idx['test'])
|
|
|
|
print('train, test, val sizes :', len(self.train), len(self.test), len(self.val))
|
|
print("[I] Finished loading.")
|
|
|
|
# form a mini batch from a given list of samples = [(graph, label) pairs]
|
|
def collate(self, samples):
|
|
# The input samples is a list of pairs (graph, label).
|
|
graphs, labels = map(list, zip(*samples))
|
|
labels = torch.stack(labels)
|
|
|
|
tab_sizes_n = [g.num_nodes() for g in graphs]
|
|
tab_snorm_n = [torch.FloatTensor(size, 1).fill_(1./size) for size in tab_sizes_n]
|
|
snorm_n = torch.cat(tab_snorm_n).sqrt()
|
|
batched_graph = dgl.batch(graphs)
|
|
|
|
return batched_graph, labels, snorm_n
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--gpu_id', default=0, type=int, help="Please give a value for gpu id")
|
|
parser.add_argument('--seed', default=41, type=int, help="Please give a value for seed")
|
|
parser.add_argument('--batch_size', default=2048, type=int, help="Please give a value for batch_size")
|
|
args = parser.parse_args()
|
|
|
|
# device
|
|
args.device = torch.device("cuda:{}".format(args.gpu_id) if torch.cuda.is_available() else "cpu")
|
|
|
|
# setting seeds
|
|
random.seed(args.seed)
|
|
np.random.seed(args.seed)
|
|
torch.manual_seed(args.seed)
|
|
if torch.cuda.is_available():
|
|
torch.cuda.manual_seed(args.seed)
|
|
|
|
dataset = PCBADataset("ogbg-molpcba")
|
|
train(dataset, args) |