项目文件夹

文件
Josh Carty 8a2b54d1d4 Add a TensorFlow example for SGC (#2480)
This is a TensorFlow implementation of Simple Graph Convolution. For
simplicity, we use the implementation of AdamW in TensorFlow Addons
that differs from Adam in PyTorch. This results in worse performance
on the evaluation datasets.

Co-authored-by: Josh Carty <joshuac@spotify.com>
Co-authored-by: Zihao Ye <expye@outlook.com>
2021-01-20 15:35:32 +08:00

182 行
5.0 KiB
Python

"""
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
)