chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:35:51 +08:00
commit c36a561cd8
2172 changed files with 455595 additions and 0 deletions
+62
View File
@@ -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}
+40
View File
@@ -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)