chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
Deep Graph Infomax (DGI)
|
||||
========================
|
||||
|
||||
- Paper link: [https://arxiv.org/abs/1809.10341](https://arxiv.org/abs/1809.10341)
|
||||
- Author's code repo (in Pytorch):
|
||||
[https://github.com/PetarV-/DGI](https://github.com/PetarV-/DGI)
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
- tensorflow 2.1+
|
||||
- requests
|
||||
|
||||
```bash
|
||||
pip install tensorflow requests
|
||||
```
|
||||
|
||||
How to run
|
||||
----------
|
||||
|
||||
Run with following:
|
||||
|
||||
```bash
|
||||
python3 train.py --dataset=cora --gpu=0 --self-loop
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 train.py --dataset=citeseer --gpu=0
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 train.py --dataset=pubmed --gpu=0
|
||||
```
|
||||
|
||||
Results
|
||||
-------
|
||||
* cora: ~81.6 (80.9-82.9) (paper: 82.3)
|
||||
* citeseer: ~70.2 (paper: 71.8)
|
||||
* pubmed: ~77.2 (paper: 76.8)
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Deep Graph Infomax in DGL
|
||||
|
||||
References
|
||||
----------
|
||||
Papers: https://arxiv.org/abs/1809.10341
|
||||
Author's code: https://github.com/PetarV-/DGI
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
from gcn import GCN
|
||||
from tensorflow.keras import layers
|
||||
|
||||
|
||||
class Encoder(layers.Layer):
|
||||
def __init__(self, g, in_feats, n_hidden, n_layers, activation, dropout):
|
||||
super(Encoder, self).__init__()
|
||||
self.g = g
|
||||
self.conv = GCN(
|
||||
g, in_feats, n_hidden, n_hidden, n_layers, activation, dropout
|
||||
)
|
||||
|
||||
def call(self, features, corrupt=False):
|
||||
if corrupt:
|
||||
perm = np.random.permutation(self.g.number_of_nodes())
|
||||
features = tf.gather(features, perm)
|
||||
features = self.conv(features)
|
||||
return features
|
||||
|
||||
|
||||
class Discriminator(layers.Layer):
|
||||
def __init__(self, n_hidden):
|
||||
super(Discriminator, self).__init__()
|
||||
uinit = tf.keras.initializers.RandomUniform(
|
||||
-1.0 / math.sqrt(n_hidden), 1.0 / math.sqrt(n_hidden)
|
||||
)
|
||||
self.weight = tf.Variable(
|
||||
initial_value=uinit(shape=(n_hidden, n_hidden), dtype="float32"),
|
||||
trainable=True,
|
||||
)
|
||||
|
||||
def call(self, features, summary):
|
||||
features = tf.matmul(
|
||||
features, tf.matmul(self.weight, tf.expand_dims(summary, -1))
|
||||
)
|
||||
return features
|
||||
|
||||
|
||||
class DGI(tf.keras.Model):
|
||||
def __init__(self, g, in_feats, n_hidden, n_layers, activation, dropout):
|
||||
super(DGI, self).__init__()
|
||||
self.encoder = Encoder(
|
||||
g, in_feats, n_hidden, n_layers, activation, dropout
|
||||
)
|
||||
self.discriminator = Discriminator(n_hidden)
|
||||
self.loss = tf.nn.sigmoid_cross_entropy_with_logits
|
||||
|
||||
def call(self, features):
|
||||
positive = self.encoder(features, corrupt=False)
|
||||
negative = self.encoder(features, corrupt=True)
|
||||
summary = tf.nn.sigmoid(tf.reduce_mean(positive, axis=0))
|
||||
|
||||
positive = self.discriminator(positive, summary)
|
||||
negative = self.discriminator(negative, summary)
|
||||
|
||||
l1 = self.loss(tf.ones(positive.shape), positive)
|
||||
l2 = self.loss(tf.zeros(negative.shape), negative)
|
||||
|
||||
return tf.reduce_mean(l1) + tf.reduce_mean(l2)
|
||||
|
||||
|
||||
class Classifier(layers.Layer):
|
||||
def __init__(self, n_hidden, n_classes):
|
||||
super(Classifier, self).__init__()
|
||||
self.fc = layers.Dense(n_classes)
|
||||
|
||||
def call(self, features):
|
||||
features = self.fc(features)
|
||||
return features
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
This code was copied from the GCN implementation in DGL examples.
|
||||
"""
|
||||
import tensorflow as tf
|
||||
|
||||
from dgl.nn.tensorflow import GraphConv
|
||||
from tensorflow.keras import layers
|
||||
|
||||
|
||||
class GCN(layers.Layer):
|
||||
def __init__(
|
||||
self, g, in_feats, n_hidden, n_classes, n_layers, activation, dropout
|
||||
):
|
||||
super(GCN, self).__init__()
|
||||
self.g = g
|
||||
self.layers = []
|
||||
# input layer
|
||||
self.layers.append(GraphConv(in_feats, n_hidden, activation=activation))
|
||||
# hidden layers
|
||||
for i in range(n_layers - 1):
|
||||
self.layers.append(
|
||||
GraphConv(n_hidden, n_hidden, activation=activation)
|
||||
)
|
||||
# output layer
|
||||
self.layers.append(GraphConv(n_hidden, n_classes))
|
||||
self.dropout = layers.Dropout(dropout)
|
||||
|
||||
def call(self, features):
|
||||
h = features
|
||||
for i, layer in enumerate(self.layers):
|
||||
if i != 0:
|
||||
h = self.dropout(h)
|
||||
h = layer(self.g, h)
|
||||
return h
|
||||
@@ -0,0 +1,225 @@
|
||||
import argparse
|
||||
import time
|
||||
|
||||
import dgl
|
||||
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
from dgi import Classifier, DGI
|
||||
from dgl.data import (
|
||||
CiteseerGraphDataset,
|
||||
CoraGraphDataset,
|
||||
PubmedGraphDataset,
|
||||
register_data_args,
|
||||
)
|
||||
from tensorflow.keras import layers
|
||||
|
||||
|
||||
def evaluate(model, features, labels, mask):
|
||||
logits = model(features, training=False)
|
||||
logits = logits[mask]
|
||||
labels = labels[mask]
|
||||
indices = tf.math.argmax(logits, axis=1)
|
||||
acc = tf.reduce_mean(tf.cast(indices == labels, dtype=tf.float32))
|
||||
return acc.numpy().item()
|
||||
|
||||
|
||||
def main(args):
|
||||
# load and preprocess dataset
|
||||
if args.dataset == "cora":
|
||||
data = CoraGraphDataset()
|
||||
elif args.dataset == "citeseer":
|
||||
data = CiteseerGraphDataset()
|
||||
elif args.dataset == "pubmed":
|
||||
data = PubmedGraphDataset()
|
||||
else:
|
||||
raise ValueError("Unknown dataset: {}".format(args.dataset))
|
||||
|
||||
g = data[0]
|
||||
if args.gpu < 0:
|
||||
device = "/cpu:0"
|
||||
else:
|
||||
device = "/gpu:{}".format(args.gpu)
|
||||
g = g.to(device)
|
||||
|
||||
with tf.device(device):
|
||||
features = g.ndata["feat"]
|
||||
labels = g.ndata["label"]
|
||||
train_mask = g.ndata["train_mask"]
|
||||
val_mask = g.ndata["val_mask"]
|
||||
test_mask = g.ndata["test_mask"]
|
||||
in_feats = features.shape[1]
|
||||
n_classes = data.num_classes
|
||||
n_edges = g.number_of_edges()
|
||||
|
||||
# add self loop
|
||||
if args.self_loop:
|
||||
g = dgl.remove_self_loop(g)
|
||||
g = dgl.add_self_loop(g)
|
||||
n_edges = g.number_of_edges()
|
||||
|
||||
# create DGI model
|
||||
dgi = DGI(
|
||||
g,
|
||||
in_feats,
|
||||
args.n_hidden,
|
||||
args.n_layers,
|
||||
tf.keras.layers.PReLU(
|
||||
alpha_initializer=tf.constant_initializer(0.25)
|
||||
),
|
||||
args.dropout,
|
||||
)
|
||||
|
||||
dgi_optimizer = tf.keras.optimizers.Adam(learning_rate=args.dgi_lr)
|
||||
|
||||
# train deep graph infomax
|
||||
cnt_wait = 0
|
||||
best = 1e9
|
||||
best_t = 0
|
||||
dur = []
|
||||
for epoch in range(args.n_dgi_epochs):
|
||||
if epoch >= 3:
|
||||
t0 = time.time()
|
||||
|
||||
with tf.GradientTape() as tape:
|
||||
loss = dgi(features)
|
||||
# Manually Weight Decay
|
||||
# We found Tensorflow has a different implementation on weight decay
|
||||
# of Adam(W) optimizer with PyTorch. And this results in worse results.
|
||||
# Manually adding weights to the loss to do weight decay solves this problem.
|
||||
for weight in dgi.trainable_weights:
|
||||
loss = loss + args.weight_decay * tf.nn.l2_loss(weight)
|
||||
grads = tape.gradient(loss, dgi.trainable_weights)
|
||||
dgi_optimizer.apply_gradients(zip(grads, dgi.trainable_weights))
|
||||
|
||||
if loss < best:
|
||||
best = loss
|
||||
best_t = epoch
|
||||
cnt_wait = 0
|
||||
dgi.save_weights("best_dgi.pkl")
|
||||
else:
|
||||
cnt_wait += 1
|
||||
|
||||
if cnt_wait == args.patience:
|
||||
print("Early stopping!")
|
||||
break
|
||||
|
||||
if epoch >= 3:
|
||||
dur.append(time.time() - t0)
|
||||
|
||||
print(
|
||||
"Epoch {:05d} | Time(s) {:.4f} | Loss {:.4f} | "
|
||||
"ETputs(KTEPS) {:.2f}".format(
|
||||
epoch,
|
||||
np.mean(dur),
|
||||
loss.numpy().item(),
|
||||
n_edges / np.mean(dur) / 1000,
|
||||
)
|
||||
)
|
||||
|
||||
# create classifier model
|
||||
classifier = Classifier(args.n_hidden, n_classes)
|
||||
|
||||
classifier_optimizer = tf.keras.optimizers.Adam(
|
||||
learning_rate=args.classifier_lr
|
||||
)
|
||||
|
||||
# train classifier
|
||||
print("Loading {}th epoch".format(best_t))
|
||||
dgi.load_weights("best_dgi.pkl")
|
||||
embeds = dgi.encoder(features, corrupt=False)
|
||||
embeds = tf.stop_gradient(embeds)
|
||||
dur = []
|
||||
loss_fcn = tf.keras.losses.SparseCategoricalCrossentropy(
|
||||
from_logits=True
|
||||
)
|
||||
for epoch in range(args.n_classifier_epochs):
|
||||
if epoch >= 3:
|
||||
t0 = time.time()
|
||||
with tf.GradientTape() as tape:
|
||||
preds = classifier(embeds)
|
||||
loss = loss_fcn(labels[train_mask], preds[train_mask])
|
||||
# Manually Weight Decay
|
||||
# We found Tensorflow has a different implementation on weight decay
|
||||
# of Adam(W) optimizer with PyTorch. And this results in worse results.
|
||||
# Manually adding weights to the loss to do weight decay solves this problem.
|
||||
# In original code, there's no weight decay applied in this part
|
||||
# link: https://github.com/PetarV-/DGI/blob/master/execute.py#L121
|
||||
# for weight in classifier.trainable_weights:
|
||||
# loss = loss + \
|
||||
# args.weight_decay * tf.nn.l2_loss(weight)
|
||||
grads = tape.gradient(loss, classifier.trainable_weights)
|
||||
classifier_optimizer.apply_gradients(
|
||||
zip(grads, classifier.trainable_weights)
|
||||
)
|
||||
if epoch >= 3:
|
||||
dur.append(time.time() - t0)
|
||||
|
||||
acc = evaluate(classifier, embeds, labels, val_mask)
|
||||
print(
|
||||
"Epoch {:05d} | Time(s) {:.4f} | Loss {:.4f} | Accuracy {:.4f} | "
|
||||
"ETputs(KTEPS) {:.2f}".format(
|
||||
epoch,
|
||||
np.mean(dur),
|
||||
loss.numpy().item(),
|
||||
acc,
|
||||
n_edges / np.mean(dur) / 1000,
|
||||
)
|
||||
)
|
||||
|
||||
print()
|
||||
acc = evaluate(classifier, embeds, labels, test_mask)
|
||||
print("Test Accuracy {:.4f}".format(acc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="DGI")
|
||||
register_data_args(parser)
|
||||
parser.add_argument(
|
||||
"--dropout", type=float, default=0.0, help="dropout probability"
|
||||
)
|
||||
parser.add_argument("--gpu", type=int, default=-1, help="gpu")
|
||||
parser.add_argument(
|
||||
"--dgi-lr", type=float, default=1e-3, help="dgi learning rate"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--classifier-lr",
|
||||
type=float,
|
||||
default=1e-2,
|
||||
help="classifier learning rate",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-dgi-epochs",
|
||||
type=int,
|
||||
default=300,
|
||||
help="number of training epochs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-classifier-epochs",
|
||||
type=int,
|
||||
default=300,
|
||||
help="number of training epochs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-hidden", type=int, default=512, help="number of hidden gcn units"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-layers", type=int, default=1, help="number of hidden gcn layers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weight-decay", type=float, default=0.0, help="Weight for L2 loss"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--patience", type=int, default=20, help="early stop patience condition"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--self-loop",
|
||||
action="store_true",
|
||||
help="graph self-loop (default=False)",
|
||||
)
|
||||
parser.set_defaults(self_loop=False)
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
main(args)
|
||||
@@ -0,0 +1,48 @@
|
||||
Graph Attention Networks (GAT)
|
||||
============
|
||||
|
||||
- Paper link: [https://arxiv.org/abs/1710.10903](https://arxiv.org/abs/1710.10903)
|
||||
- Author's code repo (in Tensorflow):
|
||||
[https://github.com/PetarV-/GAT](https://github.com/PetarV-/GAT).
|
||||
- Popular pytorch implementation:
|
||||
[https://github.com/Diego999/pyGAT](https://github.com/Diego999/pyGAT).
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
- tensorflow 2.1.0+
|
||||
- requests
|
||||
|
||||
```bash
|
||||
pip install tensorflow requests
|
||||
DGLBACKEND=tensorflow
|
||||
```
|
||||
|
||||
How to run
|
||||
----------
|
||||
|
||||
Run with following:
|
||||
|
||||
```bash
|
||||
python3 train.py --dataset=cora --gpu=0
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 train.py --dataset=citeseer --gpu=0 --early-stop
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 train.py --dataset=pubmed --gpu=0 --num-out-heads=8 --weight-decay=0.001 --early-stop
|
||||
```
|
||||
|
||||
|
||||
Results
|
||||
-------
|
||||
|
||||
| Dataset | Test Accuracy | Baseline (paper) |
|
||||
| -------- | ------------- | ---------------- |
|
||||
| Cora | 84.2 | 83.0(+-0.7) |
|
||||
| Citeseer | 70.9 | 72.5(+-0.7) |
|
||||
| Pubmed | 78.5 | 79.0(+-0.3) |
|
||||
|
||||
* All the accuracy numbers are obtained after 200 epochs.
|
||||
* All time is measured on EC2 p3.2xlarge instance w/ V100 GPU.
|
||||
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Graph Attention Networks in DGL using SPMV optimization.
|
||||
References
|
||||
----------
|
||||
Paper: https://arxiv.org/abs/1710.10903
|
||||
Author's code: https://github.com/PetarV-/GAT
|
||||
Pytorch implementation: https://github.com/Diego999/pyGAT
|
||||
"""
|
||||
|
||||
import dgl.function as fn
|
||||
import tensorflow as tf
|
||||
from dgl.nn import GATConv
|
||||
from tensorflow.keras import layers
|
||||
|
||||
|
||||
class GAT(tf.keras.Model):
|
||||
def __init__(
|
||||
self,
|
||||
g,
|
||||
num_layers,
|
||||
in_dim,
|
||||
num_hidden,
|
||||
num_classes,
|
||||
heads,
|
||||
activation,
|
||||
feat_drop,
|
||||
attn_drop,
|
||||
negative_slope,
|
||||
residual,
|
||||
):
|
||||
super(GAT, self).__init__()
|
||||
self.g = g
|
||||
self.num_layers = num_layers
|
||||
self.gat_layers = []
|
||||
self.activation = activation
|
||||
# input projection (no residual)
|
||||
self.gat_layers.append(
|
||||
GATConv(
|
||||
in_dim,
|
||||
num_hidden,
|
||||
heads[0],
|
||||
feat_drop,
|
||||
attn_drop,
|
||||
negative_slope,
|
||||
False,
|
||||
self.activation,
|
||||
)
|
||||
)
|
||||
# hidden layers
|
||||
for l in range(1, num_layers):
|
||||
# due to multi-head, the in_dim = num_hidden * num_heads
|
||||
self.gat_layers.append(
|
||||
GATConv(
|
||||
num_hidden * heads[l - 1],
|
||||
num_hidden,
|
||||
heads[l],
|
||||
feat_drop,
|
||||
attn_drop,
|
||||
negative_slope,
|
||||
residual,
|
||||
self.activation,
|
||||
)
|
||||
)
|
||||
# output projection
|
||||
self.gat_layers.append(
|
||||
GATConv(
|
||||
num_hidden * heads[-2],
|
||||
num_classes,
|
||||
heads[-1],
|
||||
feat_drop,
|
||||
attn_drop,
|
||||
negative_slope,
|
||||
residual,
|
||||
None,
|
||||
)
|
||||
)
|
||||
|
||||
def call(self, inputs):
|
||||
h = inputs
|
||||
for l in range(self.num_layers):
|
||||
h = self.gat_layers[l](self.g, h)
|
||||
h = tf.reshape(h, (h.shape[0], -1))
|
||||
# output projection
|
||||
logits = tf.reduce_mean(self.gat_layers[-1](self.g, h), axis=1)
|
||||
return logits
|
||||
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
Graph Attention Networks in DGL using SPMV optimization.
|
||||
Multiple heads are also batched together for faster training.
|
||||
Compared with the original paper, this code does not implement
|
||||
early stopping.
|
||||
References
|
||||
----------
|
||||
Paper: https://arxiv.org/abs/1710.10903
|
||||
Author's code: https://github.com/PetarV-/GAT
|
||||
Pytorch implementation: https://github.com/Diego999/pyGAT
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import time
|
||||
|
||||
import dgl
|
||||
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
from dgl.data import (
|
||||
CiteseerGraphDataset,
|
||||
CoraGraphDataset,
|
||||
PubmedGraphDataset,
|
||||
register_data_args,
|
||||
)
|
||||
from gat import GAT
|
||||
from utils import EarlyStopping
|
||||
|
||||
|
||||
def accuracy(logits, labels):
|
||||
indices = tf.math.argmax(logits, axis=1)
|
||||
acc = tf.reduce_mean(tf.cast(indices == labels, dtype=tf.float32))
|
||||
return acc.numpy().item()
|
||||
|
||||
|
||||
def evaluate(model, features, labels, mask):
|
||||
logits = model(features, training=False)
|
||||
logits = logits[mask]
|
||||
labels = labels[mask]
|
||||
return accuracy(logits, labels)
|
||||
|
||||
|
||||
def main(args):
|
||||
# load and preprocess dataset
|
||||
if args.dataset == "cora":
|
||||
data = CoraGraphDataset()
|
||||
elif args.dataset == "citeseer":
|
||||
data = CiteseerGraphDataset()
|
||||
elif args.dataset == "pubmed":
|
||||
data = PubmedGraphDataset()
|
||||
else:
|
||||
raise ValueError("Unknown dataset: {}".format(args.dataset))
|
||||
|
||||
g = data[0]
|
||||
if args.gpu < 0:
|
||||
device = "/cpu:0"
|
||||
else:
|
||||
device = "/gpu:{}".format(args.gpu)
|
||||
g = g.to(device)
|
||||
|
||||
with tf.device(device):
|
||||
features = g.ndata["feat"]
|
||||
labels = g.ndata["label"]
|
||||
train_mask = g.ndata["train_mask"]
|
||||
val_mask = g.ndata["val_mask"]
|
||||
test_mask = g.ndata["test_mask"]
|
||||
num_feats = features.shape[1]
|
||||
n_classes = data.num_classes
|
||||
n_edges = g.number_of_edges()
|
||||
print(
|
||||
"""----Data statistics------'
|
||||
#Edges %d
|
||||
#Classes %d
|
||||
#Train samples %d
|
||||
#Val samples %d
|
||||
#Test samples %d"""
|
||||
% (
|
||||
n_edges,
|
||||
n_classes,
|
||||
train_mask.numpy().sum(),
|
||||
val_mask.numpy().sum(),
|
||||
test_mask.numpy().sum(),
|
||||
)
|
||||
)
|
||||
|
||||
g = dgl.remove_self_loop(g)
|
||||
g = dgl.add_self_loop(g)
|
||||
n_edges = g.number_of_edges()
|
||||
# create model
|
||||
heads = ([args.num_heads] * args.num_layers) + [args.num_out_heads]
|
||||
model = GAT(
|
||||
g,
|
||||
args.num_layers,
|
||||
num_feats,
|
||||
args.num_hidden,
|
||||
n_classes,
|
||||
heads,
|
||||
tf.nn.elu,
|
||||
args.in_drop,
|
||||
args.attn_drop,
|
||||
args.negative_slope,
|
||||
args.residual,
|
||||
)
|
||||
print(model)
|
||||
if args.early_stop:
|
||||
stopper = EarlyStopping(patience=100)
|
||||
|
||||
# loss_fcn = tf.keras.losses.SparseCategoricalCrossentropy(
|
||||
# from_logits=False)
|
||||
loss_fcn = tf.nn.sparse_softmax_cross_entropy_with_logits
|
||||
|
||||
# use optimizer
|
||||
optimizer = tf.keras.optimizers.Adam(
|
||||
learning_rate=args.lr, epsilon=1e-8
|
||||
)
|
||||
|
||||
# initialize graph
|
||||
dur = []
|
||||
for epoch in range(args.epochs):
|
||||
if epoch >= 3:
|
||||
t0 = time.time()
|
||||
# forward
|
||||
with tf.GradientTape() as tape:
|
||||
tape.watch(model.trainable_weights)
|
||||
logits = model(features, training=True)
|
||||
loss_value = tf.reduce_mean(
|
||||
loss_fcn(
|
||||
labels=labels[train_mask], logits=logits[train_mask]
|
||||
)
|
||||
)
|
||||
# Manually Weight Decay
|
||||
# We found Tensorflow has a different implementation on weight decay
|
||||
# of Adam(W) optimizer with PyTorch. And this results in worse results.
|
||||
# Manually adding weights to the loss to do weight decay solves this problem.
|
||||
for weight in model.trainable_weights:
|
||||
loss_value = loss_value + args.weight_decay * tf.nn.l2_loss(
|
||||
weight
|
||||
)
|
||||
|
||||
grads = tape.gradient(loss_value, model.trainable_weights)
|
||||
optimizer.apply_gradients(zip(grads, model.trainable_weights))
|
||||
|
||||
if epoch >= 3:
|
||||
dur.append(time.time() - t0)
|
||||
|
||||
train_acc = accuracy(logits[train_mask], labels[train_mask])
|
||||
|
||||
if args.fastmode:
|
||||
val_acc = accuracy(logits[val_mask], labels[val_mask])
|
||||
else:
|
||||
val_acc = evaluate(model, features, labels, val_mask)
|
||||
if args.early_stop:
|
||||
if stopper.step(val_acc, model):
|
||||
break
|
||||
|
||||
print(
|
||||
"Epoch {:05d} | Time(s) {:.4f} | Loss {:.4f} | TrainAcc {:.4f} |"
|
||||
" ValAcc {:.4f} | ETputs(KTEPS) {:.2f}".format(
|
||||
epoch,
|
||||
np.mean(dur),
|
||||
loss_value.numpy().item(),
|
||||
train_acc,
|
||||
val_acc,
|
||||
n_edges / np.mean(dur) / 1000,
|
||||
)
|
||||
)
|
||||
|
||||
print()
|
||||
if args.early_stop:
|
||||
model.load_weights("es_checkpoint.pb")
|
||||
acc = evaluate(model, features, labels, test_mask)
|
||||
print("Test Accuracy {:.4f}".format(acc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="GAT")
|
||||
register_data_args(parser)
|
||||
parser.add_argument(
|
||||
"--gpu",
|
||||
type=int,
|
||||
default=-1,
|
||||
help="which GPU to use. Set -1 to use CPU.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--epochs", type=int, default=200, help="number of training epochs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-heads",
|
||||
type=int,
|
||||
default=8,
|
||||
help="number of hidden attention heads",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-out-heads",
|
||||
type=int,
|
||||
default=1,
|
||||
help="number of output attention heads",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-layers", type=int, default=1, help="number of hidden layers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-hidden", type=int, default=8, help="number of hidden units"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--residual",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="use residual connection",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--in-drop", type=float, default=0.6, help="input feature dropout"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--attn-drop", type=float, default=0.6, help="attention dropout"
|
||||
)
|
||||
parser.add_argument("--lr", type=float, default=0.005, help="learning rate")
|
||||
parser.add_argument(
|
||||
"--weight-decay", type=float, default=5e-4, help="weight decay"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--negative-slope",
|
||||
type=float,
|
||||
default=0.2,
|
||||
help="the negative slope of leaky relu",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--early-stop",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="indicates whether to use early stop or not",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fastmode",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="skip re-evaluate the validation set",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
main(args)
|
||||
@@ -0,0 +1,31 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
class EarlyStopping:
|
||||
def __init__(self, patience=10):
|
||||
self.patience = patience
|
||||
self.counter = 0
|
||||
self.best_score = None
|
||||
self.early_stop = False
|
||||
|
||||
def step(self, acc, model):
|
||||
score = acc
|
||||
if self.best_score is None:
|
||||
self.best_score = score
|
||||
self.save_checkpoint(model)
|
||||
elif score < self.best_score:
|
||||
self.counter += 1
|
||||
print(
|
||||
f"EarlyStopping counter: {self.counter} out of {self.patience}"
|
||||
)
|
||||
if self.counter >= self.patience:
|
||||
self.early_stop = True
|
||||
else:
|
||||
self.best_score = score
|
||||
self.save_checkpoint(model)
|
||||
self.counter = 0
|
||||
return self.early_stop
|
||||
|
||||
def save_checkpoint(self, model):
|
||||
"""Saves model when validation loss decrease."""
|
||||
model.save_weights("es_checkpoint.pb")
|
||||
@@ -0,0 +1,36 @@
|
||||
Graph Convolutional Networks (GCN)
|
||||
============
|
||||
|
||||
- Paper link: [https://arxiv.org/abs/1609.02907](https://arxiv.org/abs/1609.02907)
|
||||
- Author's code repo: [https://github.com/tkipf/gcn](https://github.com/tkipf/gcn). Note that the original code is
|
||||
implemented with Tensorflow for the paper.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
- Tensorflow 2.1+
|
||||
- requests
|
||||
|
||||
``bash
|
||||
pip install tensorflow requests
|
||||
export DGLBACKEND=tensorflow
|
||||
``
|
||||
|
||||
Codes
|
||||
-----
|
||||
The folder contains three implementations of GCN:
|
||||
- `gcn.py` uses DGL's predefined graph convolution module.
|
||||
- `gcn_mp.py` uses user-defined message and reduce functions.
|
||||
- `gcn_builtin.py` improves from `gcn_mp.py` by using DGL's builtin functions
|
||||
so SPMV optimization could be applied.
|
||||
|
||||
Results
|
||||
-------
|
||||
|
||||
Run with following (available dataset: "cora", "citeseer", "pubmed")
|
||||
```bash
|
||||
python3 train.py --dataset cora --gpu 0 --self-loop
|
||||
```
|
||||
|
||||
* cora: ~0.810 (0.79-0.83) (paper: 0.815)
|
||||
* citeseer: 0.707 (paper: 0.703)
|
||||
* pubmed: 0.792 (paper: 0.790)
|
||||
@@ -0,0 +1,40 @@
|
||||
"""GCN using DGL nn package
|
||||
|
||||
References:
|
||||
- Semi-Supervised Classification with Graph Convolutional Networks
|
||||
- Paper: https://arxiv.org/abs/1609.02907
|
||||
- Code: https://github.com/tkipf/gcn
|
||||
"""
|
||||
import tensorflow as tf
|
||||
|
||||
from dgl.nn.tensorflow import GraphConv
|
||||
from tensorflow.keras import layers
|
||||
|
||||
|
||||
class GCN(tf.keras.Model):
|
||||
def __init__(
|
||||
self, g, in_feats, n_hidden, n_classes, n_layers, activation, dropout
|
||||
):
|
||||
super(GCN, self).__init__()
|
||||
self.g = g
|
||||
self.layer_list = []
|
||||
# input layer
|
||||
self.layer_list.append(
|
||||
GraphConv(in_feats, n_hidden, activation=activation)
|
||||
)
|
||||
# hidden layers
|
||||
for i in range(n_layers - 1):
|
||||
self.layer_list.append(
|
||||
GraphConv(n_hidden, n_hidden, activation=activation)
|
||||
)
|
||||
# output layer
|
||||
self.layer_list.append(GraphConv(n_hidden, n_classes))
|
||||
self.dropout = layers.Dropout(dropout)
|
||||
|
||||
def call(self, features):
|
||||
h = features
|
||||
for i, layer in enumerate(self.layer_list):
|
||||
if i != 0:
|
||||
h = self.dropout(h)
|
||||
h = layer(self.g, h)
|
||||
return h
|
||||
@@ -0,0 +1,225 @@
|
||||
import argparse
|
||||
import math
|
||||
import time
|
||||
|
||||
import dgl
|
||||
import dgl.function as fn
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
from dgl.data import (
|
||||
CiteseerGraphDataset,
|
||||
CoraGraphDataset,
|
||||
PubmedGraphDataset,
|
||||
register_data_args,
|
||||
)
|
||||
from tensorflow.keras import layers
|
||||
|
||||
|
||||
class GCNLayer(layers.Layer):
|
||||
def __init__(self, g, in_feats, out_feats, activation, dropout, bias=True):
|
||||
super(GCNLayer, self).__init__()
|
||||
self.g = g
|
||||
|
||||
w_init = tf.keras.initializers.VarianceScaling(
|
||||
scale=1.0, mode="fan_out", distribution="uniform"
|
||||
)
|
||||
self.weight = tf.Variable(
|
||||
initial_value=w_init(shape=(in_feats, out_feats), dtype="float32"),
|
||||
trainable=True,
|
||||
)
|
||||
if dropout:
|
||||
self.dropout = layers.Dropout(rate=dropout)
|
||||
else:
|
||||
self.dropout = 0.0
|
||||
if bias:
|
||||
b_init = tf.zeros_initializer()
|
||||
self.bias = tf.Variable(
|
||||
initial_value=b_init(shape=(out_feats,), dtype="float32"),
|
||||
trainable=True,
|
||||
)
|
||||
else:
|
||||
self.bias = None
|
||||
self.activation = activation
|
||||
|
||||
def call(self, h):
|
||||
if self.dropout:
|
||||
h = self.dropout(h)
|
||||
self.g.ndata["h"] = tf.matmul(h, self.weight)
|
||||
self.g.ndata["norm_h"] = self.g.ndata["h"] * self.g.ndata["norm"]
|
||||
self.g.update_all(fn.copy_u("norm_h", "m"), fn.sum("m", "h"))
|
||||
h = self.g.ndata["h"]
|
||||
if self.bias is not None:
|
||||
h = h + self.bias
|
||||
if self.activation:
|
||||
h = self.activation(h)
|
||||
return h
|
||||
|
||||
|
||||
class GCN(layers.Layer):
|
||||
def __init__(
|
||||
self, g, in_feats, n_hidden, n_classes, n_layers, activation, dropout
|
||||
):
|
||||
super(GCN, self).__init__()
|
||||
self.layers = []
|
||||
|
||||
# input layer
|
||||
self.layers.append(GCNLayer(g, in_feats, n_hidden, activation, dropout))
|
||||
# hidden layers
|
||||
for i in range(n_layers - 1):
|
||||
self.layers.append(
|
||||
GCNLayer(g, n_hidden, n_hidden, activation, dropout)
|
||||
)
|
||||
# output layer
|
||||
self.layers.append(GCNLayer(g, n_hidden, n_classes, None, dropout))
|
||||
|
||||
def call(self, features):
|
||||
h = features
|
||||
for layer in self.layers:
|
||||
h = layer(h)
|
||||
return h
|
||||
|
||||
|
||||
def evaluate(model, features, labels, mask):
|
||||
logits = model(features, training=False)
|
||||
logits = logits[mask]
|
||||
labels = labels[mask]
|
||||
indices = tf.math.argmax(logits, axis=1)
|
||||
acc = tf.reduce_mean(tf.cast(indices == labels, dtype=tf.float32))
|
||||
return acc.numpy().item()
|
||||
|
||||
|
||||
def main(args):
|
||||
# load and preprocess dataset
|
||||
if args.dataset == "cora":
|
||||
data = CoraGraphDataset()
|
||||
elif args.dataset == "citeseer":
|
||||
data = CiteseerGraphDataset()
|
||||
elif args.dataset == "pubmed":
|
||||
data = PubmedGraphDataset()
|
||||
else:
|
||||
raise ValueError("Unknown dataset: {}".format(args.dataset))
|
||||
|
||||
g = data[0]
|
||||
if args.gpu < 0:
|
||||
device = "/cpu:0"
|
||||
else:
|
||||
device = "/gpu:{}".format(args.gpu)
|
||||
g = g.to(device)
|
||||
|
||||
with tf.device(device):
|
||||
features = g.ndata["feat"]
|
||||
labels = g.ndata["label"]
|
||||
train_mask = g.ndata["train_mask"]
|
||||
val_mask = g.ndata["val_mask"]
|
||||
test_mask = g.ndata["test_mask"]
|
||||
in_feats = features.shape[1]
|
||||
n_classes = data.num_classes
|
||||
n_edges = data.graph.number_of_edges()
|
||||
print(
|
||||
"""----Data statistics------'
|
||||
#Edges %d
|
||||
#Classes %d
|
||||
#Train samples %d
|
||||
#Val samples %d
|
||||
#Test samples %d"""
|
||||
% (
|
||||
n_edges,
|
||||
n_classes,
|
||||
train_mask.numpy().sum(),
|
||||
val_mask.numpy().sum(),
|
||||
test_mask.numpy().sum(),
|
||||
)
|
||||
)
|
||||
|
||||
# add self loop
|
||||
g = dgl.remove_self_loop(g)
|
||||
g = dgl.add_self_loop(g)
|
||||
n_edges = g.number_of_edges()
|
||||
# # normalization
|
||||
degs = tf.cast(tf.identity(g.in_degrees()), dtype=tf.float32)
|
||||
norm = tf.math.pow(degs, -0.5)
|
||||
norm = tf.where(tf.math.is_inf(norm), tf.zeros_like(norm), norm)
|
||||
|
||||
g.ndata["norm"] = tf.expand_dims(norm, -1)
|
||||
|
||||
# create GCN model
|
||||
model = GCN(
|
||||
g,
|
||||
in_feats,
|
||||
args.n_hidden,
|
||||
n_classes,
|
||||
args.n_layers,
|
||||
tf.nn.relu,
|
||||
args.dropout,
|
||||
)
|
||||
|
||||
optimizer = tf.keras.optimizers.Adam(learning_rate=args.lr)
|
||||
|
||||
loss_fcn = tf.keras.losses.SparseCategoricalCrossentropy(
|
||||
from_logits=True
|
||||
)
|
||||
# initialize graph
|
||||
dur = []
|
||||
for epoch in range(args.n_epochs):
|
||||
if epoch >= 3:
|
||||
t0 = time.time()
|
||||
# forward
|
||||
with tf.GradientTape() as tape:
|
||||
logits = model(features)
|
||||
loss_value = loss_fcn(labels[train_mask], logits[train_mask])
|
||||
# Manually Weight Decay
|
||||
# We found Tensorflow has a different implementation on weight decay
|
||||
# of Adam(W) optimizer with PyTorch. And this results in worse results.
|
||||
# Manually adding weights to the loss to do weight decay solves this problem.
|
||||
for weight in model.trainable_weights:
|
||||
loss_value = loss_value + args.weight_decay * tf.nn.l2_loss(
|
||||
weight
|
||||
)
|
||||
|
||||
grads = tape.gradient(loss_value, model.trainable_weights)
|
||||
optimizer.apply_gradients(zip(grads, model.trainable_weights))
|
||||
|
||||
if epoch >= 3:
|
||||
dur.append(time.time() - t0)
|
||||
|
||||
acc = evaluate(model, features, labels, val_mask)
|
||||
print(
|
||||
"Epoch {:05d} | Time(s) {:.4f} | Loss {:.4f} | Accuracy {:.4f} | "
|
||||
"ETputs(KTEPS) {:.2f}".format(
|
||||
epoch,
|
||||
np.mean(dur),
|
||||
loss_value.numpy().item(),
|
||||
acc,
|
||||
n_edges / np.mean(dur) / 1000,
|
||||
)
|
||||
)
|
||||
|
||||
acc = evaluate(model, features, labels, test_mask)
|
||||
print("Test Accuracy {:.4f}".format(acc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="GCN")
|
||||
register_data_args(parser)
|
||||
parser.add_argument(
|
||||
"--dropout", type=float, default=0.5, help="dropout probability"
|
||||
)
|
||||
parser.add_argument("--gpu", type=int, default=-1, help="gpu")
|
||||
parser.add_argument("--lr", type=float, default=1e-2, help="learning rate")
|
||||
parser.add_argument(
|
||||
"--n-epochs", type=int, default=200, help="number of training epochs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-hidden", type=int, default=16, help="number of hidden gcn units"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-layers", type=int, default=1, help="number of hidden gcn layers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weight-decay", type=float, default=5e-4, help="Weight for L2 loss"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
main(args)
|
||||
@@ -0,0 +1,238 @@
|
||||
import argparse
|
||||
import math
|
||||
import time
|
||||
|
||||
import dgl
|
||||
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
from dgl.data import (
|
||||
CiteseerGraphDataset,
|
||||
CoraGraphDataset,
|
||||
PubmedGraphDataset,
|
||||
register_data_args,
|
||||
)
|
||||
from tensorflow.keras import layers
|
||||
|
||||
|
||||
def gcn_msg(edge):
|
||||
msg = edge.src["h"] * edge.src["norm"]
|
||||
return {"m": msg}
|
||||
|
||||
|
||||
def gcn_reduce(node):
|
||||
accum = tf.reduce_sum(node.mailbox["m"], 1) * node.data["norm"]
|
||||
return {"h": accum}
|
||||
|
||||
|
||||
class GCNLayer(layers.Layer):
|
||||
def __init__(self, g, in_feats, out_feats, activation, dropout, bias=True):
|
||||
super(GCNLayer, self).__init__()
|
||||
self.g = g
|
||||
|
||||
w_init = tf.random_normal_initializer()
|
||||
self.weight = tf.Variable(
|
||||
initial_value=w_init(shape=(in_feats, out_feats), dtype="float32"),
|
||||
trainable=True,
|
||||
)
|
||||
if dropout:
|
||||
self.dropout = layers.Dropout(rate=dropout)
|
||||
else:
|
||||
self.dropout = 0.0
|
||||
if bias:
|
||||
b_init = tf.zeros_initializer()
|
||||
self.bias = tf.Variable(
|
||||
initial_value=b_init(shape=(out_feats,), dtype="float32"),
|
||||
trainable=True,
|
||||
)
|
||||
else:
|
||||
self.bias = None
|
||||
self.activation = activation
|
||||
|
||||
def call(self, h):
|
||||
if self.dropout:
|
||||
h = self.dropout(h)
|
||||
self.g.ndata["h"] = tf.matmul(h, self.weight)
|
||||
self.g.update_all(gcn_msg, gcn_reduce)
|
||||
h = self.g.ndata["h"]
|
||||
if self.bias is not None:
|
||||
h = h + self.bias
|
||||
if self.activation:
|
||||
h = self.activation(h)
|
||||
return h
|
||||
|
||||
|
||||
class GCN(layers.Layer):
|
||||
def __init__(
|
||||
self, g, in_feats, n_hidden, n_classes, n_layers, activation, dropout
|
||||
):
|
||||
super(GCN, self).__init__()
|
||||
self.layers = []
|
||||
|
||||
# input layer
|
||||
self.layers.append(GCNLayer(g, in_feats, n_hidden, activation, dropout))
|
||||
# hidden layers
|
||||
for i in range(n_layers - 1):
|
||||
self.layers.append(
|
||||
GCNLayer(g, n_hidden, n_hidden, activation, dropout)
|
||||
)
|
||||
# output layer
|
||||
self.layers.append(GCNLayer(g, n_hidden, n_classes, None, dropout))
|
||||
|
||||
def call(self, features):
|
||||
h = features
|
||||
for layer in self.layers:
|
||||
h = layer(h)
|
||||
return h
|
||||
|
||||
|
||||
def evaluate(model, features, labels, mask):
|
||||
logits = model(features, training=False)
|
||||
logits = logits[mask]
|
||||
labels = labels[mask]
|
||||
indices = tf.math.argmax(logits, axis=1)
|
||||
acc = tf.reduce_mean(tf.cast(indices == labels, dtype=tf.float32))
|
||||
return acc.numpy().item()
|
||||
|
||||
|
||||
def main(args):
|
||||
# load and preprocess dataset
|
||||
if args.dataset == "cora":
|
||||
data = CoraGraphDataset()
|
||||
elif args.dataset == "citeseer":
|
||||
data = CiteseerGraphDataset()
|
||||
elif args.dataset == "pubmed":
|
||||
data = PubmedGraphDataset()
|
||||
else:
|
||||
raise ValueError("Unknown dataset: {}".format(args.dataset))
|
||||
|
||||
g = data[0]
|
||||
if args.gpu < 0:
|
||||
device = "/cpu:0"
|
||||
else:
|
||||
device = "/gpu:{}".format(args.gpu)
|
||||
g = g.to(device)
|
||||
|
||||
with tf.device(device):
|
||||
features = g.ndata["feat"]
|
||||
labels = g.ndata["label"]
|
||||
train_mask = g.ndata["train_mask"]
|
||||
val_mask = g.ndata["val_mask"]
|
||||
test_mask = g.ndata["test_mask"]
|
||||
in_feats = features.shape[1]
|
||||
n_classes = data.num_classes
|
||||
n_edges = data.graph.number_of_edges()
|
||||
print(
|
||||
"""----Data statistics------'
|
||||
#Edges %d
|
||||
#Classes %d
|
||||
#Train samples %d
|
||||
#Val samples %d
|
||||
#Test samples %d"""
|
||||
% (
|
||||
n_edges,
|
||||
n_classes,
|
||||
train_mask.numpy().sum(),
|
||||
val_mask.numpy().sum(),
|
||||
test_mask.numpy().sum(),
|
||||
)
|
||||
)
|
||||
|
||||
# add self loop
|
||||
if args.self_loop:
|
||||
g = dgl.remove_self_loop(g)
|
||||
g = dgl.add_self_loop(g)
|
||||
n_edges = g.number_of_edges()
|
||||
n_edges = g.number_of_edges()
|
||||
# # normalization
|
||||
degs = tf.cast(tf.identity(g.in_degrees()), dtype=tf.float32)
|
||||
norm = tf.math.pow(degs, -0.5)
|
||||
norm = tf.where(tf.math.is_inf(norm), tf.zeros_like(norm), norm)
|
||||
|
||||
g.ndata["norm"] = tf.expand_dims(norm, -1)
|
||||
|
||||
# create GCN model
|
||||
model = GCN(
|
||||
g,
|
||||
in_feats,
|
||||
args.n_hidden,
|
||||
n_classes,
|
||||
args.n_layers,
|
||||
tf.nn.relu,
|
||||
args.dropout,
|
||||
)
|
||||
|
||||
optimizer = tf.keras.optimizers.Adam(learning_rate=args.lr)
|
||||
|
||||
loss_fcn = tf.keras.losses.SparseCategoricalCrossentropy(
|
||||
from_logits=True
|
||||
)
|
||||
# initialize graph
|
||||
dur = []
|
||||
for epoch in range(args.n_epochs):
|
||||
if epoch >= 3:
|
||||
t0 = time.time()
|
||||
# forward
|
||||
with tf.GradientTape() as tape:
|
||||
logits = model(features)
|
||||
loss_value = loss_fcn(labels[train_mask], logits[train_mask])
|
||||
# Manually Weight Decay
|
||||
# We found Tensorflow has a different implementation on weight decay
|
||||
# of Adam(W) optimizer with PyTorch. And this results in worse results.
|
||||
# Manually adding weights to the loss to do weight decay solves this problem.
|
||||
for weight in model.trainable_weights:
|
||||
loss_value = loss_value + args.weight_decay * tf.nn.l2_loss(
|
||||
weight
|
||||
)
|
||||
grads = tape.gradient(loss_value, model.trainable_weights)
|
||||
optimizer.apply_gradients(zip(grads, model.trainable_weights))
|
||||
|
||||
if epoch >= 3:
|
||||
dur.append(time.time() - t0)
|
||||
|
||||
acc = evaluate(model, features, labels, val_mask)
|
||||
print(
|
||||
"Epoch {:05d} | Time(s) {:.4f} | Loss {:.4f} | Accuracy {:.4f} | "
|
||||
"ETputs(KTEPS) {:.2f}".format(
|
||||
epoch,
|
||||
np.mean(dur),
|
||||
loss_value.numpy().item(),
|
||||
acc,
|
||||
n_edges / np.mean(dur) / 1000,
|
||||
)
|
||||
)
|
||||
|
||||
acc = evaluate(model, features, labels, test_mask)
|
||||
print("Test Accuracy {:.4f}".format(acc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="GCN")
|
||||
register_data_args(parser)
|
||||
parser.add_argument(
|
||||
"--dropout", type=float, default=0.5, help="dropout probability"
|
||||
)
|
||||
parser.add_argument("--gpu", type=int, default=-1, help="gpu")
|
||||
parser.add_argument("--lr", type=float, default=1e-2, help="learning rate")
|
||||
parser.add_argument(
|
||||
"--n-epochs", type=int, default=200, help="number of training epochs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-hidden", type=int, default=16, help="number of hidden gcn units"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-layers", type=int, default=1, help="number of hidden gcn layers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weight-decay", type=float, default=5e-4, help="Weight for L2 loss"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--self-loop",
|
||||
action="store_true",
|
||||
help="graph self-loop (default=False)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
main(args)
|
||||
@@ -0,0 +1,168 @@
|
||||
import argparse
|
||||
import time
|
||||
|
||||
import dgl
|
||||
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
from dgl.data import CiteseerGraphDataset, CoraGraphDataset, PubmedGraphDataset
|
||||
from gcn import GCN
|
||||
|
||||
|
||||
def evaluate(model, features, labels, mask):
|
||||
logits = model(features, training=False)
|
||||
logits = logits[mask]
|
||||
labels = labels[mask]
|
||||
indices = tf.math.argmax(logits, axis=1)
|
||||
acc = tf.reduce_mean(tf.cast(indices == labels, dtype=tf.float32))
|
||||
return acc.numpy().item()
|
||||
|
||||
|
||||
def main(args):
|
||||
# load and preprocess dataset
|
||||
if args.dataset == "cora":
|
||||
data = CoraGraphDataset()
|
||||
elif args.dataset == "citeseer":
|
||||
data = CiteseerGraphDataset()
|
||||
elif args.dataset == "pubmed":
|
||||
data = PubmedGraphDataset()
|
||||
else:
|
||||
raise ValueError("Unknown dataset: {}".format(args.dataset))
|
||||
|
||||
g = data[0]
|
||||
if args.gpu < 0:
|
||||
device = "/cpu:0"
|
||||
else:
|
||||
device = "/gpu:{}".format(args.gpu)
|
||||
g = g.to(device)
|
||||
|
||||
with tf.device(device):
|
||||
features = g.ndata["feat"]
|
||||
labels = g.ndata["label"]
|
||||
train_mask = g.ndata["train_mask"]
|
||||
val_mask = g.ndata["val_mask"]
|
||||
test_mask = g.ndata["test_mask"]
|
||||
in_feats = features.shape[1]
|
||||
n_classes = data.num_classes
|
||||
n_edges = g.number_of_edges()
|
||||
print(
|
||||
"""----Data statistics------'
|
||||
#Edges %d
|
||||
#Classes %d
|
||||
#Train samples %d
|
||||
#Val samples %d
|
||||
#Test samples %d"""
|
||||
% (
|
||||
n_edges,
|
||||
n_classes,
|
||||
train_mask.numpy().sum(),
|
||||
val_mask.numpy().sum(),
|
||||
test_mask.numpy().sum(),
|
||||
)
|
||||
)
|
||||
|
||||
# add self loop
|
||||
if args.self_loop:
|
||||
g = dgl.remove_self_loop(g)
|
||||
g = dgl.add_self_loop(g)
|
||||
n_edges = g.number_of_edges()
|
||||
# normalization
|
||||
degs = tf.cast(tf.identity(g.in_degrees()), dtype=tf.float32)
|
||||
norm = tf.math.pow(degs, -0.5)
|
||||
norm = tf.where(tf.math.is_inf(norm), tf.zeros_like(norm), norm)
|
||||
|
||||
g.ndata["norm"] = tf.expand_dims(norm, -1)
|
||||
|
||||
# create GCN model
|
||||
model = GCN(
|
||||
g,
|
||||
in_feats,
|
||||
args.n_hidden,
|
||||
n_classes,
|
||||
args.n_layers,
|
||||
tf.nn.relu,
|
||||
args.dropout,
|
||||
)
|
||||
|
||||
loss_fcn = tf.keras.losses.SparseCategoricalCrossentropy(
|
||||
from_logits=True
|
||||
)
|
||||
# use optimizer
|
||||
optimizer = tf.keras.optimizers.Adam(
|
||||
learning_rate=args.lr, epsilon=1e-8
|
||||
)
|
||||
|
||||
# initialize graph
|
||||
dur = []
|
||||
for epoch in range(args.n_epochs):
|
||||
if epoch >= 3:
|
||||
t0 = time.time()
|
||||
# forward
|
||||
with tf.GradientTape() as tape:
|
||||
logits = model(features)
|
||||
loss_value = loss_fcn(labels[train_mask], logits[train_mask])
|
||||
# Manually Weight Decay
|
||||
# We found Tensorflow has a different implementation on weight decay
|
||||
# of Adam(W) optimizer with PyTorch. And this results in worse results.
|
||||
# Manually adding weights to the loss to do weight decay solves this problem.
|
||||
for weight in model.trainable_weights:
|
||||
loss_value = loss_value + args.weight_decay * tf.nn.l2_loss(
|
||||
weight
|
||||
)
|
||||
|
||||
grads = tape.gradient(loss_value, model.trainable_weights)
|
||||
optimizer.apply_gradients(zip(grads, model.trainable_weights))
|
||||
if epoch >= 3:
|
||||
dur.append(time.time() - t0)
|
||||
|
||||
acc = evaluate(model, features, labels, val_mask)
|
||||
print(
|
||||
"Epoch {:05d} | Time(s) {:.4f} | Loss {:.4f} | Accuracy {:.4f} | "
|
||||
"ETputs(KTEPS) {:.2f}".format(
|
||||
epoch,
|
||||
np.mean(dur),
|
||||
loss_value.numpy().item(),
|
||||
acc,
|
||||
n_edges / np.mean(dur) / 1000,
|
||||
)
|
||||
)
|
||||
|
||||
acc = evaluate(model, features, labels, test_mask)
|
||||
print("Test Accuracy {:.4f}".format(acc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="GCN")
|
||||
parser.add_argument(
|
||||
"--dataset",
|
||||
type=str,
|
||||
default="cora",
|
||||
help="Dataset name ('cora', 'citeseer', 'pubmed').",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dropout", type=float, default=0.5, help="dropout probability"
|
||||
)
|
||||
parser.add_argument("--gpu", type=int, default=-1, help="gpu")
|
||||
parser.add_argument("--lr", type=float, default=1e-2, help="learning rate")
|
||||
parser.add_argument(
|
||||
"--n-epochs", type=int, default=200, help="number of training epochs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-hidden", type=int, default=16, help="number of hidden gcn units"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-layers", type=int, default=1, help="number of hidden gcn layers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weight-decay", type=float, default=5e-4, help="Weight for L2 loss"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--self-loop",
|
||||
action="store_true",
|
||||
help="graph self-loop (default=False)",
|
||||
)
|
||||
parser.set_defaults(self_loop=False)
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
main(args)
|
||||
@@ -0,0 +1,34 @@
|
||||
# Relational-GCN
|
||||
|
||||
* Paper: [https://arxiv.org/abs/1703.06103](https://arxiv.org/abs/1703.06103)
|
||||
* Author's code for entity classification: [https://github.com/tkipf/relational-gcn](https://github.com/tkipf/relational-gcn)
|
||||
* Author's code for link prediction: [https://github.com/MichSchli/RelationPrediction](https://github.com/MichSchli/RelationPrediction)
|
||||
|
||||
### Dependencies
|
||||
* Tensorflow 2.2+
|
||||
* requests
|
||||
* rdflib
|
||||
* pandas
|
||||
|
||||
```
|
||||
pip install requests tensorflow rdflib pandas
|
||||
export DGLBACKEND=tensorflow
|
||||
```
|
||||
|
||||
Example code was tested with rdflib 4.2.2 and pandas 0.23.4
|
||||
|
||||
### Entity Classification
|
||||
AIFB: accuracy 92.78% (5 runs, DGL), 95.83% (paper)
|
||||
```
|
||||
python3 entity_classify.py -d aifb --testing --gpu 0
|
||||
```
|
||||
|
||||
MUTAG: accuracy 71.47% (5 runs, DGL), 73.23% (paper)
|
||||
```
|
||||
python3 entity_classify.py -d mutag --l2norm 5e-4 --n-bases 30 --testing --gpu 0
|
||||
```
|
||||
|
||||
BGS: accuracy 93.10% (5 runs, DGL n-base=25), 83.10% (paper n-base=40)
|
||||
```
|
||||
python3 entity_classify.py -d bgs --l2norm 5e-4 --n-bases 25 --testing --gpu 0
|
||||
```
|
||||
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Modeling Relational Data with Graph Convolutional Networks
|
||||
Paper: https://arxiv.org/abs/1703.06103
|
||||
Code: https://github.com/tkipf/relational-gcn
|
||||
|
||||
Difference compared to tkipf/relation-gcn
|
||||
* l2norm applied to all weights
|
||||
* remove nodes that won't be touched
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import time
|
||||
from functools import partial
|
||||
|
||||
import dgl
|
||||
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
from dgl.data.rdf import AIFBDataset, AMDataset, BGSDataset, MUTAGDataset
|
||||
from dgl.nn.tensorflow import RelGraphConv
|
||||
from model import BaseRGCN
|
||||
from tensorflow.keras import layers
|
||||
|
||||
|
||||
class EntityClassify(BaseRGCN):
|
||||
def create_features(self):
|
||||
features = tf.range(self.num_nodes)
|
||||
return features
|
||||
|
||||
def build_input_layer(self):
|
||||
return RelGraphConv(
|
||||
self.num_nodes,
|
||||
self.h_dim,
|
||||
self.num_rels,
|
||||
"basis",
|
||||
self.num_bases,
|
||||
activation=tf.nn.relu,
|
||||
self_loop=self.use_self_loop,
|
||||
dropout=self.dropout,
|
||||
)
|
||||
|
||||
def build_hidden_layer(self, idx):
|
||||
return RelGraphConv(
|
||||
self.h_dim,
|
||||
self.h_dim,
|
||||
self.num_rels,
|
||||
"basis",
|
||||
self.num_bases,
|
||||
activation=tf.nn.relu,
|
||||
self_loop=self.use_self_loop,
|
||||
dropout=self.dropout,
|
||||
)
|
||||
|
||||
def build_output_layer(self):
|
||||
return RelGraphConv(
|
||||
self.h_dim,
|
||||
self.out_dim,
|
||||
self.num_rels,
|
||||
"basis",
|
||||
self.num_bases,
|
||||
activation=partial(tf.nn.softmax, axis=1),
|
||||
self_loop=self.use_self_loop,
|
||||
)
|
||||
|
||||
|
||||
def acc(logits, labels, mask):
|
||||
logits = tf.gather(logits, mask)
|
||||
labels = tf.gather(labels, mask)
|
||||
indices = tf.math.argmax(logits, axis=1)
|
||||
acc = tf.reduce_mean(tf.cast(indices == labels, dtype=tf.float32))
|
||||
return acc
|
||||
|
||||
|
||||
def main(args):
|
||||
# load graph data
|
||||
if args.dataset == "aifb":
|
||||
dataset = AIFBDataset()
|
||||
elif args.dataset == "mutag":
|
||||
dataset = MUTAGDataset()
|
||||
elif args.dataset == "bgs":
|
||||
dataset = BGSDataset()
|
||||
elif args.dataset == "am":
|
||||
dataset = AMDataset()
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
# preprocessing in cpu
|
||||
with tf.device("/cpu:0"):
|
||||
# Load from hetero-graph
|
||||
hg = dataset[0]
|
||||
|
||||
num_rels = len(hg.canonical_etypes)
|
||||
category = dataset.predict_category
|
||||
num_classes = dataset.num_classes
|
||||
train_mask = hg.nodes[category].data.pop("train_mask")
|
||||
test_mask = hg.nodes[category].data.pop("test_mask")
|
||||
train_idx = tf.squeeze(tf.where(train_mask))
|
||||
test_idx = tf.squeeze(tf.where(test_mask))
|
||||
labels = hg.nodes[category].data.pop("labels")
|
||||
|
||||
# split dataset into train, validate, test
|
||||
if args.validation:
|
||||
val_idx = train_idx[: len(train_idx) // 5]
|
||||
train_idx = train_idx[len(train_idx) // 5 :]
|
||||
else:
|
||||
val_idx = train_idx
|
||||
|
||||
# calculate norm for each edge type and store in edge
|
||||
for canonical_etype in hg.canonical_etypes:
|
||||
u, v, eid = hg.all_edges(form="all", etype=canonical_etype)
|
||||
_, inverse_index, count = tf.unique_with_counts(v)
|
||||
degrees = tf.gather(count, inverse_index)
|
||||
norm = tf.ones(eid.shape[0]) / tf.cast(degrees, tf.float32)
|
||||
norm = tf.expand_dims(norm, 1)
|
||||
hg.edges[canonical_etype].data["norm"] = norm
|
||||
|
||||
# get target category id
|
||||
category_id = len(hg.ntypes)
|
||||
for i, ntype in enumerate(hg.ntypes):
|
||||
if ntype == category:
|
||||
category_id = i
|
||||
|
||||
# edge type and normalization factor
|
||||
g = dgl.to_homogeneous(hg, edata=["norm"])
|
||||
|
||||
# check cuda
|
||||
if args.gpu < 0:
|
||||
device = "/cpu:0"
|
||||
use_cuda = False
|
||||
else:
|
||||
device = "/gpu:{}".format(args.gpu)
|
||||
g = g.to(device)
|
||||
use_cuda = True
|
||||
num_nodes = g.number_of_nodes()
|
||||
node_ids = tf.range(num_nodes, dtype=tf.int64)
|
||||
edge_norm = g.edata["norm"]
|
||||
edge_type = tf.cast(g.edata[dgl.ETYPE], tf.int64)
|
||||
|
||||
# find out the target node ids in g
|
||||
node_tids = g.ndata[dgl.NTYPE]
|
||||
loc = node_tids == category_id
|
||||
target_idx = tf.squeeze(tf.where(loc))
|
||||
|
||||
# since the nodes are featureless, the input feature is then the node id.
|
||||
feats = tf.range(num_nodes, dtype=tf.int64)
|
||||
|
||||
with tf.device(device):
|
||||
# create model
|
||||
model = EntityClassify(
|
||||
num_nodes,
|
||||
args.n_hidden,
|
||||
num_classes,
|
||||
num_rels,
|
||||
num_bases=args.n_bases,
|
||||
num_hidden_layers=args.n_layers - 2,
|
||||
dropout=args.dropout,
|
||||
use_self_loop=args.use_self_loop,
|
||||
use_cuda=use_cuda,
|
||||
)
|
||||
|
||||
# optimizer
|
||||
optimizer = tf.keras.optimizers.Adam(learning_rate=args.lr)
|
||||
# training loop
|
||||
print("start training...")
|
||||
forward_time = []
|
||||
backward_time = []
|
||||
loss_fcn = tf.keras.losses.SparseCategoricalCrossentropy(
|
||||
from_logits=False
|
||||
)
|
||||
for epoch in range(args.n_epochs):
|
||||
t0 = time.time()
|
||||
with tf.GradientTape() as tape:
|
||||
logits = model(g, feats, edge_type, edge_norm)
|
||||
logits = tf.gather(logits, target_idx)
|
||||
loss = loss_fcn(
|
||||
tf.gather(labels, train_idx), tf.gather(logits, train_idx)
|
||||
)
|
||||
# Manually Weight Decay
|
||||
# We found Tensorflow has a different implementation on weight decay
|
||||
# of Adam(W) optimizer with PyTorch. And this results in worse results.
|
||||
# Manually adding weights to the loss to do weight decay solves this problem.
|
||||
for weight in model.trainable_weights:
|
||||
loss = loss + args.l2norm * tf.nn.l2_loss(weight)
|
||||
t1 = time.time()
|
||||
grads = tape.gradient(loss, model.trainable_weights)
|
||||
optimizer.apply_gradients(zip(grads, model.trainable_weights))
|
||||
t2 = time.time()
|
||||
|
||||
forward_time.append(t1 - t0)
|
||||
backward_time.append(t2 - t1)
|
||||
print(
|
||||
"Epoch {:05d} | Train Forward Time(s) {:.4f} | Backward Time(s) {:.4f}".format(
|
||||
epoch, forward_time[-1], backward_time[-1]
|
||||
)
|
||||
)
|
||||
train_acc = acc(logits, labels, train_idx)
|
||||
val_loss = loss_fcn(
|
||||
tf.gather(labels, val_idx), tf.gather(logits, val_idx)
|
||||
)
|
||||
val_acc = acc(logits, labels, val_idx)
|
||||
print(
|
||||
"Train Accuracy: {:.4f} | Train Loss: {:.4f} | Validation Accuracy: {:.4f} | Validation loss: {:.4f}".format(
|
||||
train_acc,
|
||||
loss.numpy().item(),
|
||||
val_acc,
|
||||
val_loss.numpy().item(),
|
||||
)
|
||||
)
|
||||
print()
|
||||
|
||||
logits = model(g, feats, edge_type, edge_norm)
|
||||
logits = tf.gather(logits, target_idx)
|
||||
test_loss = loss_fcn(
|
||||
tf.gather(labels, test_idx), tf.gather(logits, test_idx)
|
||||
)
|
||||
test_acc = acc(logits, labels, test_idx)
|
||||
print(
|
||||
"Test Accuracy: {:.4f} | Test loss: {:.4f}".format(
|
||||
test_acc, test_loss.numpy().item()
|
||||
)
|
||||
)
|
||||
print()
|
||||
|
||||
print(
|
||||
"Mean forward time: {:4f}".format(
|
||||
np.mean(forward_time[len(forward_time) // 4 :])
|
||||
)
|
||||
)
|
||||
print(
|
||||
"Mean backward time: {:4f}".format(
|
||||
np.mean(backward_time[len(backward_time) // 4 :])
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="RGCN")
|
||||
parser.add_argument(
|
||||
"--dropout", type=float, default=0, help="dropout probability"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-hidden", type=int, default=16, help="number of hidden units"
|
||||
)
|
||||
parser.add_argument("--gpu", type=int, default=-1, help="gpu")
|
||||
parser.add_argument("--lr", type=float, default=1e-2, help="learning rate")
|
||||
parser.add_argument(
|
||||
"--n-bases",
|
||||
type=int,
|
||||
default=-1,
|
||||
help="number of filter weight matrices, default: -1 [use all]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-layers", type=int, default=2, help="number of propagation rounds"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--n-epochs",
|
||||
type=int,
|
||||
default=50,
|
||||
help="number of training epochs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--dataset", type=str, required=True, help="dataset to use"
|
||||
)
|
||||
parser.add_argument("--l2norm", type=float, default=0, help="l2 norm coef")
|
||||
parser.add_argument(
|
||||
"--use-self-loop",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="include self feature as a special relation",
|
||||
)
|
||||
fp = parser.add_mutually_exclusive_group(required=False)
|
||||
fp.add_argument("--validation", dest="validation", action="store_true")
|
||||
fp.add_argument("--testing", dest="validation", action="store_false")
|
||||
parser.set_defaults(validation=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
args.bfs_level = args.n_layers + 1 # pruning used nodes for memory
|
||||
main(args)
|
||||
@@ -0,0 +1,59 @@
|
||||
import tensorflow as tf
|
||||
from tensorflow.keras import layers
|
||||
|
||||
|
||||
class BaseRGCN(layers.Layer):
|
||||
def __init__(
|
||||
self,
|
||||
num_nodes,
|
||||
h_dim,
|
||||
out_dim,
|
||||
num_rels,
|
||||
num_bases,
|
||||
num_hidden_layers=1,
|
||||
dropout=0,
|
||||
use_self_loop=False,
|
||||
use_cuda=False,
|
||||
):
|
||||
super(BaseRGCN, self).__init__()
|
||||
self.num_nodes = num_nodes
|
||||
self.h_dim = h_dim
|
||||
self.out_dim = out_dim
|
||||
self.num_rels = num_rels
|
||||
self.num_bases = None if num_bases < 0 else num_bases
|
||||
self.num_hidden_layers = num_hidden_layers
|
||||
self.dropout = dropout
|
||||
self.use_self_loop = use_self_loop
|
||||
self.use_cuda = use_cuda
|
||||
|
||||
# create rgcn layers
|
||||
self.build_model()
|
||||
|
||||
def build_model(self):
|
||||
self.layers = []
|
||||
# i2h
|
||||
i2h = self.build_input_layer()
|
||||
if i2h is not None:
|
||||
self.layers.append(i2h)
|
||||
# h2h
|
||||
for idx in range(self.num_hidden_layers):
|
||||
h2h = self.build_hidden_layer(idx)
|
||||
self.layers.append(h2h)
|
||||
# h2o
|
||||
h2o = self.build_output_layer()
|
||||
if h2o is not None:
|
||||
self.layers.append(h2o)
|
||||
|
||||
def build_input_layer(self):
|
||||
return None
|
||||
|
||||
def build_hidden_layer(self, idx):
|
||||
raise NotImplementedError
|
||||
|
||||
def build_output_layer(self):
|
||||
return None
|
||||
|
||||
def call(self, g, h, r, norm):
|
||||
for layer in self.layers:
|
||||
h = layer(g, h, r, norm)
|
||||
return h
|
||||
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Utility functions for link prediction
|
||||
Most code is adapted from authors' implementation of RGCN link prediction:
|
||||
https://github.com/MichSchli/RelationPrediction
|
||||
|
||||
"""
|
||||
|
||||
import dgl
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
|
||||
#######################################################################
|
||||
#
|
||||
# Utility function for building training and testing graphs
|
||||
#
|
||||
#######################################################################
|
||||
|
||||
|
||||
def get_adj_and_degrees(num_nodes, triplets):
|
||||
"""Get adjacency list and degrees of the graph"""
|
||||
adj_list = [[] for _ in range(num_nodes)]
|
||||
for i, triplet in enumerate(triplets):
|
||||
adj_list[triplet[0]].append([i, triplet[2]])
|
||||
adj_list[triplet[2]].append([i, triplet[0]])
|
||||
|
||||
degrees = np.array([len(a) for a in adj_list])
|
||||
adj_list = [np.array(a) for a in adj_list]
|
||||
return adj_list, degrees
|
||||
|
||||
|
||||
def sample_edge_neighborhood(adj_list, degrees, n_triplets, sample_size):
|
||||
"""Sample edges by neighborhool expansion.
|
||||
|
||||
This guarantees that the sampled edges form a connected graph, which
|
||||
may help deeper GNNs that require information from more than one hop.
|
||||
"""
|
||||
edges = np.zeros((sample_size), dtype=np.int32)
|
||||
|
||||
# initialize
|
||||
sample_counts = np.array([d for d in degrees])
|
||||
picked = np.array([False for _ in range(n_triplets)])
|
||||
seen = np.array([False for _ in degrees])
|
||||
|
||||
for i in range(0, sample_size):
|
||||
weights = sample_counts * seen
|
||||
|
||||
if np.sum(weights) == 0:
|
||||
weights = np.ones_like(weights)
|
||||
weights[np.where(sample_counts == 0)] = 0
|
||||
|
||||
probabilities = (weights) / np.sum(weights)
|
||||
chosen_vertex = np.random.choice(
|
||||
np.arange(degrees.shape[0]), p=probabilities
|
||||
)
|
||||
chosen_adj_list = adj_list[chosen_vertex]
|
||||
seen[chosen_vertex] = True
|
||||
|
||||
chosen_edge = np.random.choice(np.arange(chosen_adj_list.shape[0]))
|
||||
chosen_edge = chosen_adj_list[chosen_edge]
|
||||
edge_number = chosen_edge[0]
|
||||
|
||||
while picked[edge_number]:
|
||||
chosen_edge = np.random.choice(np.arange(chosen_adj_list.shape[0]))
|
||||
chosen_edge = chosen_adj_list[chosen_edge]
|
||||
edge_number = chosen_edge[0]
|
||||
|
||||
edges[i] = edge_number
|
||||
other_vertex = chosen_edge[1]
|
||||
picked[edge_number] = True
|
||||
sample_counts[chosen_vertex] -= 1
|
||||
sample_counts[other_vertex] -= 1
|
||||
seen[other_vertex] = True
|
||||
|
||||
return edges
|
||||
|
||||
|
||||
def sample_edge_uniform(adj_list, degrees, n_triplets, sample_size):
|
||||
"""Sample edges uniformly from all the edges."""
|
||||
all_edges = np.arange(n_triplets)
|
||||
return np.random.choice(all_edges, sample_size, replace=False)
|
||||
|
||||
|
||||
def generate_sampled_graph_and_labels(
|
||||
triplets,
|
||||
sample_size,
|
||||
split_size,
|
||||
num_rels,
|
||||
adj_list,
|
||||
degrees,
|
||||
negative_rate,
|
||||
sampler="uniform",
|
||||
):
|
||||
"""Get training graph and signals
|
||||
First perform edge neighborhood sampling on graph, then perform negative
|
||||
sampling to generate negative samples
|
||||
"""
|
||||
# perform edge neighbor sampling
|
||||
if sampler == "uniform":
|
||||
edges = sample_edge_uniform(
|
||||
adj_list, degrees, len(triplets), sample_size
|
||||
)
|
||||
elif sampler == "neighbor":
|
||||
edges = sample_edge_neighborhood(
|
||||
adj_list, degrees, len(triplets), sample_size
|
||||
)
|
||||
else:
|
||||
raise ValueError("Sampler type must be either 'uniform' or 'neighbor'.")
|
||||
|
||||
# relabel nodes to have consecutive node ids
|
||||
edges = triplets[edges]
|
||||
src, rel, dst = edges.transpose()
|
||||
uniq_v, edges = np.unique((src, dst), return_inverse=True)
|
||||
src, dst = np.reshape(edges, (2, -1))
|
||||
relabeled_edges = np.stack((src, rel, dst)).transpose()
|
||||
|
||||
# negative sampling
|
||||
samples, labels = negative_sampling(
|
||||
relabeled_edges, len(uniq_v), negative_rate
|
||||
)
|
||||
|
||||
# further split graph, only half of the edges will be used as graph
|
||||
# structure, while the rest half is used as unseen positive samples
|
||||
split_size = int(sample_size * split_size)
|
||||
graph_split_ids = np.random.choice(
|
||||
np.arange(sample_size), size=split_size, replace=False
|
||||
)
|
||||
src = src[graph_split_ids]
|
||||
dst = dst[graph_split_ids]
|
||||
rel = rel[graph_split_ids]
|
||||
|
||||
# build DGL graph
|
||||
print("# sampled nodes: {}".format(len(uniq_v)))
|
||||
print("# sampled edges: {}".format(len(src) * 2))
|
||||
g, rel, norm = build_graph_from_triplets(
|
||||
len(uniq_v), num_rels, (src, rel, dst)
|
||||
)
|
||||
return g, uniq_v, rel, norm, samples, labels
|
||||
|
||||
|
||||
def comp_deg_norm(g):
|
||||
g = g.local_var()
|
||||
in_deg = g.in_degrees(range(g.number_of_nodes())).float().numpy()
|
||||
norm = 1.0 / in_deg
|
||||
norm[np.isinf(norm)] = 0
|
||||
return norm
|
||||
|
||||
|
||||
def build_graph_from_triplets(num_nodes, num_rels, triplets):
|
||||
"""Create a DGL graph. The graph is bidirectional because RGCN authors
|
||||
use reversed relations.
|
||||
This function also generates edge type and normalization factor
|
||||
(reciprocal of node incoming degree)
|
||||
"""
|
||||
g = dgl.DGLGraph()
|
||||
g.add_nodes(num_nodes)
|
||||
src, rel, dst = triplets
|
||||
src, dst = np.concatenate((src, dst)), np.concatenate((dst, src))
|
||||
rel = np.concatenate((rel, rel + num_rels))
|
||||
edges = sorted(zip(dst, src, rel))
|
||||
dst, src, rel = np.array(edges).transpose()
|
||||
g.add_edges(src, dst)
|
||||
norm = comp_deg_norm(g)
|
||||
print("# nodes: {}, # edges: {}".format(num_nodes, len(src)))
|
||||
return g, rel, norm
|
||||
|
||||
|
||||
def build_test_graph(num_nodes, num_rels, edges):
|
||||
src, rel, dst = edges.transpose()
|
||||
print("Test graph:")
|
||||
return build_graph_from_triplets(num_nodes, num_rels, (src, rel, dst))
|
||||
|
||||
|
||||
def negative_sampling(pos_samples, num_entity, negative_rate):
|
||||
size_of_batch = len(pos_samples)
|
||||
num_to_generate = size_of_batch * negative_rate
|
||||
neg_samples = np.tile(pos_samples, (negative_rate, 1))
|
||||
labels = np.zeros(size_of_batch * (negative_rate + 1), dtype=np.float32)
|
||||
labels[:size_of_batch] = 1
|
||||
values = np.random.randint(num_entity, size=num_to_generate)
|
||||
choices = np.random.uniform(size=num_to_generate)
|
||||
subj = choices > 0.5
|
||||
obj = choices <= 0.5
|
||||
neg_samples[subj, 0] = values[subj]
|
||||
neg_samples[obj, 2] = values[obj]
|
||||
|
||||
return np.concatenate((pos_samples, neg_samples)), labels
|
||||
@@ -0,0 +1,63 @@
|
||||
# Simple Graph Convolution (SGC)
|
||||
|
||||
> Graph Convolutional Networks derive inspiration primarily from recent deep learning approaches, and as a result, may inherit unnecessary complexity and redundant computation. In this paper, we reduce this excess complexity through successively removing nonlinearities and collapsing weight matrices between consecutive layers. We theoretically analyze the resulting linear model and show that it corresponds to a fixed low-pass filter followed by a linear classifier.
|
||||
|
||||
* [Paper](https://arxiv.org/abs/1902.07153)
|
||||
* [Author Implementation](https://github.com/Tiiiger/SGC)
|
||||
|
||||
Note: TensorFlow uses a different implementation of weight decay in AdamW to PyTorch. This results in differences in performance. You can see this by manually adding the L2 of the weights to the loss like [this](https://github.com/dmlc/dgl/blob/d696558b0bbcb60f1c4cf68dc93cd22c1077ce06/examples/tensorflow/gcn/train.py#L99) for comparison.
|
||||
|
||||
## Requirements
|
||||
|
||||
This example is tested with TensorFlow 2.3.0.
|
||||
|
||||
```bash
|
||||
$ pip install dgl tensorflow tensorflow_addons
|
||||
```
|
||||
|
||||
## Usage
|
||||
```bash
|
||||
$ python sgc.py --help
|
||||
usage: sgc.py [-h] [--dataset DATASET] [--lr LR] [--bias]
|
||||
[--n-epochs N_EPOCHS] [--weight-decay WEIGHT_DECAY]
|
||||
|
||||
Run experiment for Simple Graph Convolution (SGC)
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--dataset DATASET dataset to run
|
||||
--lr LR learning rate
|
||||
--bias flag to use bias
|
||||
--n-epochs N_EPOCHS number of training epochs
|
||||
--weight-decay WEIGHT_DECAY weight for L2 loss
|
||||
```
|
||||
|
||||
## Results
|
||||
```bash
|
||||
# Cora citation network dataset
|
||||
$ python sgc.py --dataset cora --lr 0.2 --n-epochs 100 --weight-decay 5e-6
|
||||
...
|
||||
Epoch 100/100
|
||||
1/1 [==============================] - 0s 40ms/step - loss: 0.0313 - accuracy: 1.0000 - val_loss: 0.7870 - val_accuracy: 0.7620
|
||||
Test Accuracy: 77.2%
|
||||
|
||||
# Citeseer citation network dataset
|
||||
$ python sgc.py --dataset citeseer --lr 0.2 --n-epochs 150 --bias --weight-decay 5e-5
|
||||
...
|
||||
Epoch 150/150
|
||||
1/1 [==============================] - 0s 65ms/step - loss: 0.0160 - accuracy: 1.0000 - val_loss: 1.1021 - val_accuracy: 0.6420
|
||||
Test Accuracy: 63.9%
|
||||
|
||||
# Pubmed citation network dataset
|
||||
$ python sgc.py --dataset pubmed --lr 0.2 --n-epochs 100 --bias --weight-decay 5e-5
|
||||
...
|
||||
Epoch 100/100
|
||||
1/1 [==============================] - 0s 52ms/step - loss: 0.0421 - accuracy: 1.0000 - val_loss: 0.5862 - val_accuracy: 0.7680
|
||||
Test Accuracy: 76.3%
|
||||
```
|
||||
|
||||
| Dataset | Accuracy | Paper |
|
||||
|----------|----------|-------|
|
||||
| Cora | 77.3% | 81.0% |
|
||||
| Citeseer | 63.9% | 71.9% |
|
||||
| Pubmed | 76.4% | 78.9% |
|
||||
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
This code was modified from implementations of SGC in other backends.
|
||||
|
||||
Simplifying Graph Convolutional Networks (Wu, Zhang and Souza et al, 2019)
|
||||
Paper: https://arxiv.org/abs/1902.07153
|
||||
Author Implementation: https://github.com/Tiiiger/SGC
|
||||
|
||||
SGC implementation in DGL.
|
||||
"""
|
||||
import argparse
|
||||
import textwrap
|
||||
|
||||
import tensorflow as tf
|
||||
import tensorflow_addons as tfa
|
||||
|
||||
from dgl.data import CiteseerGraphDataset, CoraGraphDataset, PubmedGraphDataset
|
||||
from dgl.nn.tensorflow.conv import SGConv
|
||||
|
||||
_DATASETS = {
|
||||
"citeseer": CiteseerGraphDataset(verbose=False),
|
||||
"cora": CoraGraphDataset(verbose=False),
|
||||
"pubmed": PubmedGraphDataset(verbose=False),
|
||||
}
|
||||
|
||||
|
||||
def load_data(dataset):
|
||||
return _DATASETS[dataset]
|
||||
|
||||
|
||||
def _sum_boolean_tensor(x):
|
||||
return tf.reduce_sum(tf.cast(x, dtype="int64"))
|
||||
|
||||
|
||||
def describe_data(data):
|
||||
g = data[0]
|
||||
|
||||
n_edges = g.number_of_edges()
|
||||
num_classes = data.num_classes
|
||||
|
||||
train_mask = g.ndata["train_mask"]
|
||||
val_mask = g.ndata["val_mask"]
|
||||
test_mask = g.ndata["test_mask"]
|
||||
|
||||
description = textwrap.dedent(
|
||||
f"""
|
||||
----Data statistics----
|
||||
Edges {n_edges:,.0f}
|
||||
Classes {num_classes:,.0f}
|
||||
Train samples {_sum_boolean_tensor(train_mask):,.0f}
|
||||
Val samples {_sum_boolean_tensor(val_mask):,.0f}
|
||||
Test samples {_sum_boolean_tensor(test_mask):,.0f}
|
||||
"""
|
||||
)
|
||||
return description
|
||||
|
||||
|
||||
class SGC(tf.keras.Model):
|
||||
def __init__(self, g, num_classes, bias=False):
|
||||
super().__init__()
|
||||
self.num_classes = num_classes
|
||||
self.g = self.ensure_self_loop(g)
|
||||
self.conv = SGConv(
|
||||
in_feats=self.in_feats,
|
||||
out_feats=self.num_classes,
|
||||
k=2,
|
||||
cached=True,
|
||||
bias=bias,
|
||||
)
|
||||
|
||||
def call(self, inputs):
|
||||
return self.conv(self.g, inputs)
|
||||
|
||||
@property
|
||||
def in_feats(self):
|
||||
return self.g.ndata["feat"].shape[1]
|
||||
|
||||
@property
|
||||
def num_nodes(self):
|
||||
return self.g.num_nodes()
|
||||
|
||||
@staticmethod
|
||||
def ensure_self_loop(g):
|
||||
g = g.remove_self_loop()
|
||||
g = g.add_self_loop()
|
||||
return g
|
||||
|
||||
def train_step(self, data):
|
||||
X, y = data
|
||||
mask = self.g.ndata["train_mask"]
|
||||
|
||||
with tf.GradientTape() as tape:
|
||||
y_pred = self(X, training=True)
|
||||
loss = self.compiled_loss(y[mask], y_pred[mask])
|
||||
|
||||
trainable_variables = self.trainable_variables
|
||||
gradients = tape.gradient(loss, trainable_variables)
|
||||
self.optimizer.apply_gradients(zip(gradients, trainable_variables))
|
||||
self.compiled_metrics.update_state(y[mask], y_pred[mask])
|
||||
return {m.name: m.result() for m in self.metrics}
|
||||
|
||||
def test_step(self, data):
|
||||
X, y = data
|
||||
mask = self.g.ndata["val_mask"]
|
||||
y_pred = self(X, training=False)
|
||||
self.compiled_loss(y[mask], y_pred[mask])
|
||||
self.compiled_metrics.update_state(y[mask], y_pred[mask])
|
||||
return {m.name: m.result() for m in self.metrics}
|
||||
|
||||
def compile(self, *args, **kwargs):
|
||||
super().compile(*args, **kwargs, run_eagerly=True)
|
||||
|
||||
def fit(self, *args, **kwargs):
|
||||
kwargs["batch_size"] = self.num_nodes
|
||||
kwargs["shuffle"] = False
|
||||
super().fit(*args, **kwargs)
|
||||
|
||||
def predict(self, *args, **kwargs):
|
||||
kwargs["batch_size"] = self.num_nodes
|
||||
return super().predict(*args, **kwargs)
|
||||
|
||||
|
||||
def main(dataset, lr, bias, n_epochs, weight_decay):
|
||||
data = load_data(dataset)
|
||||
print(describe_data(data))
|
||||
|
||||
g = data[0]
|
||||
X = g.ndata["feat"]
|
||||
y = g.ndata["label"]
|
||||
|
||||
model = SGC(g=g, num_classes=data.num_classes, bias=bias)
|
||||
|
||||
loss = tf.losses.SparseCategoricalCrossentropy(from_logits=True)
|
||||
optimizer = tfa.optimizers.AdamW(weight_decay, lr)
|
||||
accuracy = tf.metrics.SparseCategoricalAccuracy(name="accuracy")
|
||||
|
||||
model.compile(optimizer, loss, metrics=[accuracy])
|
||||
model.fit(x=X, y=y, epochs=n_epochs, validation_data=(X, y))
|
||||
|
||||
y_pred = model.predict(X, batch_size=len(X))
|
||||
test_mask = g.ndata["test_mask"]
|
||||
test_accuracy = accuracy(y[test_mask], y_pred[test_mask])
|
||||
print(f"Test Accuracy: {test_accuracy:.1%}")
|
||||
|
||||
|
||||
def _parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run experiment for Simple Graph Convolution (SGC)"
|
||||
)
|
||||
parser.add_argument("--dataset", default="cora", help="dataset to run")
|
||||
parser.add_argument("--lr", type=float, default=0.2, help="learning rate")
|
||||
parser.add_argument(
|
||||
"--bias", action="store_true", default=False, help="flag to use bias"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-epochs", type=int, default=100, help="number of training epochs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weight-decay", type=float, default=5e-6, help="weight for L2 loss"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = _parse_args()
|
||||
main(
|
||||
dataset=args.dataset,
|
||||
lr=args.lr,
|
||||
bias=args.bias,
|
||||
n_epochs=args.n_epochs,
|
||||
weight_decay=args.weight_decay,
|
||||
)
|
||||
Reference in New Issue
Block a user