chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# DGL Implementation of GNNExplainer
|
||||
|
||||
This is a DGL example for [GNNExplainer: Generating Explanations for Graph Neural Networks](https://arxiv.org/abs/1903.03894). For the authors' original implementation,
|
||||
see [here](https://github.com/RexYing/gnn-model-explainer).
|
||||
|
||||
Contributors:
|
||||
- [Jian Zhang](https://github.com/zhjwy9343)
|
||||
- [Kounianhua Du](https://github.com/KounianhuaDu)
|
||||
- [Yanjun Zhao](https://github.com/zyj-111)
|
||||
|
||||
Datasets
|
||||
----------------------
|
||||
|
||||
Four built-in synthetic datasets are used in this example.
|
||||
|
||||
- [BA-SHAPES](https://docs.dgl.ai/generated/dgl.data.BAShapeDataset.html#dgl.data.BAShapeDataset)
|
||||
- [BA-COMMUNITY](https://docs.dgl.ai/generated/dgl.data.BACommunityDataset.html#dgl.data.BACommunityDataset)
|
||||
- [TREE-CYCLE](https://docs.dgl.ai/generated/dgl.data.TreeCycleDataset.html#dgl.data.TreeCycleDataset)
|
||||
- [TREE-GRID](https://docs.dgl.ai/generated/dgl.data.TreeGridDataset.html#dgl.data.TreeGridDataset)
|
||||
|
||||
Usage
|
||||
----------------------
|
||||
|
||||
**First**, train a GNN model on a dataset.
|
||||
|
||||
```bash
|
||||
python train_main.py --dataset $DATASET
|
||||
```
|
||||
|
||||
Valid options for `$DATASET`: `BAShape`, `BACommunity`, `TreeCycle`, `TreeGrid`
|
||||
|
||||
The trained model weights will be saved to `model_{dataset}.pth`
|
||||
|
||||
**Second**, install [GNNLens2](https://github.com/dmlc/GNNLens2) with
|
||||
|
||||
```bash
|
||||
pip install -U flask-cors
|
||||
pip install Flask==2.0.3
|
||||
pip install gnnlens
|
||||
```
|
||||
|
||||
**Third**, explain the trained model with the same dataset
|
||||
|
||||
```bash
|
||||
python explain_main.py --dataset $DATASET
|
||||
```
|
||||
|
||||
**Finally**, launch `GNNLens2` to visualize the explanations
|
||||
|
||||
```bash
|
||||
gnnlens --logdir gnn_subgraph
|
||||
```
|
||||
|
||||
By entering `localhost:7777` in your web browser address bar, you can see the GNNLens2 interface. `7777` is the default port GNNLens2 uses. You can specify an alternative one by adding `--port xxxx` after the command line and change the address in the web browser accordingly.
|
||||
|
||||
A sample visualization is available below. For more details of using `GNNLens2`, check its [tutorials](https://github.com/dmlc/GNNLens2#tutorials).
|
||||
|
||||
<p align="center">
|
||||
<img src="https://data.dgl.ai/asset/image/explain_BAShape.png" width="600">
|
||||
<br>
|
||||
<b>Figure</b>: Explanation for node 41 of BAShape
|
||||
</p>
|
||||
@@ -0,0 +1,84 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
import dgl
|
||||
|
||||
import torch as th
|
||||
from dgl import load_graphs
|
||||
from dgl.data import (
|
||||
BACommunityDataset,
|
||||
BAShapeDataset,
|
||||
TreeCycleDataset,
|
||||
TreeGridDataset,
|
||||
)
|
||||
from dgl.nn import GNNExplainer
|
||||
from gnnlens import Writer
|
||||
from models import Model
|
||||
|
||||
|
||||
def main(args):
|
||||
if args.dataset == "BAShape":
|
||||
dataset = BAShapeDataset(seed=0)
|
||||
elif args.dataset == "BACommunity":
|
||||
dataset = BACommunityDataset(seed=0)
|
||||
elif args.dataset == "TreeCycle":
|
||||
dataset = TreeCycleDataset(seed=0)
|
||||
elif args.dataset == "TreeGrid":
|
||||
dataset = TreeGridDataset(seed=0)
|
||||
|
||||
graph = dataset[0]
|
||||
labels = graph.ndata["label"]
|
||||
feats = graph.ndata["feat"]
|
||||
num_classes = dataset.num_classes
|
||||
|
||||
# load an existing model
|
||||
model_path = os.path.join("./", f"model_{args.dataset}.pth")
|
||||
model_stat_dict = th.load(model_path)
|
||||
model = Model(feats.shape[-1], num_classes)
|
||||
model.load_state_dict(model_stat_dict)
|
||||
|
||||
# Choose the first node of the class 1 for explaining prediction
|
||||
target_class = 1
|
||||
for n_idx, n_label in enumerate(labels):
|
||||
if n_label == target_class:
|
||||
break
|
||||
|
||||
explainer = GNNExplainer(model, num_hops=3)
|
||||
new_center, sub_graph, feat_mask, edge_mask = explainer.explain_node(
|
||||
n_idx, graph, feats
|
||||
)
|
||||
|
||||
# gnnlens2
|
||||
# Specify the path to create a new directory for dumping data files.
|
||||
writer = Writer("gnn_subgraph")
|
||||
writer.add_graph(
|
||||
name=args.dataset,
|
||||
graph=graph,
|
||||
nlabels=labels,
|
||||
num_nlabel_types=num_classes,
|
||||
)
|
||||
writer.add_subgraph(
|
||||
graph_name=args.dataset,
|
||||
subgraph_name="GNNExplainer",
|
||||
node_id=n_idx,
|
||||
subgraph_nids=sub_graph.ndata[dgl.NID],
|
||||
subgraph_eids=sub_graph.edata[dgl.EID],
|
||||
subgraph_eweights=edge_mask,
|
||||
)
|
||||
|
||||
# Finish dumping.
|
||||
writer.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Demo of GNN explainer in DGL")
|
||||
parser.add_argument(
|
||||
"--dataset",
|
||||
type=str,
|
||||
default="BAShape",
|
||||
choices=["BAShape", "BACommunity", "TreeCycle", "TreeGrid"],
|
||||
)
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
main(args)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"models": [], "success": true}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"subgraphs": [{"id": 1, "name": "GNNExplainer"}], "success": true}
|
||||
@@ -0,0 +1 @@
|
||||
{"datasets": [{"id": 1, "name": "BAShape"}], "success": true}
|
||||
@@ -0,0 +1,40 @@
|
||||
import dgl.function as fn
|
||||
import torch as th
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class Layer(nn.Module):
|
||||
def __init__(self, in_dim, out_dim):
|
||||
super().__init__()
|
||||
self.layer = nn.Linear(in_dim * 2, out_dim, bias=True)
|
||||
|
||||
def forward(self, graph, feat, eweight=None):
|
||||
with graph.local_scope():
|
||||
graph.ndata["h"] = feat
|
||||
|
||||
if eweight is None:
|
||||
graph.update_all(fn.copy_u("h", "m"), fn.mean("m", "h"))
|
||||
else:
|
||||
graph.edata["ew"] = eweight
|
||||
graph.update_all(fn.u_mul_e("h", "ew", "m"), fn.mean("m", "h"))
|
||||
|
||||
h = self.layer(th.cat([graph.ndata["h"], feat], dim=-1))
|
||||
|
||||
return h
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, in_dim, out_dim, hid_dim=40):
|
||||
super().__init__()
|
||||
self.in_layer = Layer(in_dim, hid_dim)
|
||||
self.hid_layer = Layer(hid_dim, hid_dim)
|
||||
self.out_layer = Layer(hid_dim, out_dim)
|
||||
|
||||
def forward(self, graph, feat, eweight=None):
|
||||
h = self.in_layer(graph, feat.float(), eweight)
|
||||
h = F.relu(h)
|
||||
h = self.hid_layer(graph, h, eweight)
|
||||
h = F.relu(h)
|
||||
h = self.out_layer(graph, h, eweight)
|
||||
return h
|
||||
@@ -0,0 +1,67 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
import torch as th
|
||||
import torch.nn as nn
|
||||
|
||||
from dgl import save_graphs
|
||||
|
||||
from dgl.data import (
|
||||
BACommunityDataset,
|
||||
BAShapeDataset,
|
||||
TreeCycleDataset,
|
||||
TreeGridDataset,
|
||||
)
|
||||
from models import Model
|
||||
|
||||
|
||||
def main(args):
|
||||
if args.dataset == "BAShape":
|
||||
dataset = BAShapeDataset(seed=0)
|
||||
elif args.dataset == "BACommunity":
|
||||
dataset = BACommunityDataset(seed=0)
|
||||
elif args.dataset == "TreeCycle":
|
||||
dataset = TreeCycleDataset(seed=0)
|
||||
elif args.dataset == "TreeGrid":
|
||||
dataset = TreeGridDataset(seed=0)
|
||||
|
||||
graph = dataset[0]
|
||||
labels = graph.ndata["label"]
|
||||
n_feats = graph.ndata["feat"]
|
||||
num_classes = dataset.num_classes
|
||||
|
||||
model = Model(n_feats.shape[-1], num_classes)
|
||||
loss_fn = nn.CrossEntropyLoss()
|
||||
optim = th.optim.Adam(model.parameters(), lr=0.001)
|
||||
|
||||
for epoch in range(500):
|
||||
model.train()
|
||||
# For demo purpose, we train the model on all datapoints
|
||||
# In practice, you should train only on the training datapoints
|
||||
logits = model(graph, n_feats)
|
||||
loss = loss_fn(logits, labels)
|
||||
acc = th.sum(logits.argmax(dim=1) == labels).item() / len(labels)
|
||||
|
||||
optim.zero_grad()
|
||||
loss.backward()
|
||||
optim.step()
|
||||
|
||||
print(f"In Epoch: {epoch}; Acc: {acc}; Loss: {loss.item()}")
|
||||
|
||||
model_stat_dict = model.state_dict()
|
||||
model_path = os.path.join("./", f"model_{args.dataset}.pth")
|
||||
th.save(model_stat_dict, model_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Dummy model training")
|
||||
parser.add_argument(
|
||||
"--dataset",
|
||||
type=str,
|
||||
default="BAShape",
|
||||
choices=["BAShape", "BACommunity", "TreeCycle", "TreeGrid"],
|
||||
)
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
main(args)
|
||||
Reference in New Issue
Block a user