chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
.. _model-gcn:
|
||||
|
||||
Graph Convolutional Network
|
||||
====================================
|
||||
|
||||
**Author:** `Qi Huang <https://github.com/HQ01>`_, `Minjie Wang <https://jermainewang.github.io/>`_,
|
||||
Yu Gai, Quan Gan, Zheng Zhang
|
||||
|
||||
.. warning::
|
||||
|
||||
The tutorial aims at gaining insights into the paper, with code as a mean
|
||||
of explanation. The implementation thus is NOT optimized for running
|
||||
efficiency. For recommended implementation, please refer to the `official
|
||||
examples <https://github.com/dmlc/dgl/tree/master/examples>`_.
|
||||
|
||||
This is a gentle introduction of using DGL to implement Graph Convolutional
|
||||
Networks (Kipf & Welling et al., `Semi-Supervised Classification with Graph
|
||||
Convolutional Networks <https://arxiv.org/pdf/1609.02907.pdf>`_). We explain
|
||||
what is under the hood of the :class:`~dgl.nn.GraphConv` module.
|
||||
The reader is expected to learn how to define a new GNN layer using DGL's
|
||||
message passing APIs.
|
||||
"""
|
||||
|
||||
###############################################################################
|
||||
# Model Overview
|
||||
# ------------------------------------------
|
||||
# GCN from the perspective of message passing
|
||||
# ```````````````````````````````````````````````
|
||||
# We describe a layer of graph convolutional neural network from a message
|
||||
# passing perspective; the math can be found `here <math_>`_.
|
||||
# It boils down to the following step, for each node :math:`u`:
|
||||
#
|
||||
# 1) Aggregate neighbors' representations :math:`h_{v}` to produce an
|
||||
# intermediate representation :math:`\hat{h}_u`. 2) Transform the aggregated
|
||||
# representation :math:`\hat{h}_{u}` with a linear projection followed by a
|
||||
# non-linearity: :math:`h_{u} = f(W_{u} \hat{h}_u)`.
|
||||
#
|
||||
# We will implement step 1 with DGL message passing, and step 2 by
|
||||
# PyTorch ``nn.Module``.
|
||||
#
|
||||
# GCN implementation with DGL
|
||||
# ``````````````````````````````````````````
|
||||
# We first define the message and reduce function as usual. Since the
|
||||
# aggregation on a node :math:`u` only involves summing over the neighbors'
|
||||
# representations :math:`h_v`, we can simply use builtin functions:
|
||||
|
||||
import os
|
||||
|
||||
os.environ["DGLBACKEND"] = "pytorch"
|
||||
import dgl
|
||||
import dgl.function as fn
|
||||
import torch as th
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from dgl import DGLGraph
|
||||
|
||||
gcn_msg = fn.copy_u(u="h", out="m")
|
||||
gcn_reduce = fn.sum(msg="m", out="h")
|
||||
|
||||
###############################################################################
|
||||
# We then proceed to define the GCNLayer module. A GCNLayer essentially performs
|
||||
# message passing on all the nodes then applies a fully-connected layer.
|
||||
#
|
||||
# .. note::
|
||||
#
|
||||
# This is showing how to implement a GCN from scratch. DGL provides a more
|
||||
# efficient :class:`builtin GCN layer module <dgl.nn.pytorch.conv.GraphConv>`.
|
||||
#
|
||||
|
||||
|
||||
class GCNLayer(nn.Module):
|
||||
def __init__(self, in_feats, out_feats):
|
||||
super(GCNLayer, self).__init__()
|
||||
self.linear = nn.Linear(in_feats, out_feats)
|
||||
|
||||
def forward(self, g, feature):
|
||||
# Creating a local scope so that all the stored ndata and edata
|
||||
# (such as the `'h'` ndata below) are automatically popped out
|
||||
# when the scope exits.
|
||||
with g.local_scope():
|
||||
g.ndata["h"] = feature
|
||||
g.update_all(gcn_msg, gcn_reduce)
|
||||
h = g.ndata["h"]
|
||||
return self.linear(h)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# The forward function is essentially the same as any other commonly seen NNs
|
||||
# model in PyTorch. We can initialize GCN like any ``nn.Module``. For example,
|
||||
# let's define a simple neural network consisting of two GCN layers. Suppose we
|
||||
# are training the classifier for the cora dataset (the input feature size is
|
||||
# 1433 and the number of classes is 7). The last GCN layer computes node embeddings,
|
||||
# so the last layer in general does not apply activation.
|
||||
|
||||
|
||||
class Net(nn.Module):
|
||||
def __init__(self):
|
||||
super(Net, self).__init__()
|
||||
self.layer1 = GCNLayer(1433, 16)
|
||||
self.layer2 = GCNLayer(16, 7)
|
||||
|
||||
def forward(self, g, features):
|
||||
x = F.relu(self.layer1(g, features))
|
||||
x = self.layer2(g, x)
|
||||
return x
|
||||
|
||||
|
||||
net = Net()
|
||||
print(net)
|
||||
|
||||
###############################################################################
|
||||
# We load the cora dataset using DGL's built-in data module.
|
||||
|
||||
from dgl.data import CoraGraphDataset
|
||||
|
||||
|
||||
def load_cora_data():
|
||||
dataset = CoraGraphDataset()
|
||||
g = dataset[0]
|
||||
features = g.ndata["feat"]
|
||||
labels = g.ndata["label"]
|
||||
train_mask = g.ndata["train_mask"]
|
||||
test_mask = g.ndata["test_mask"]
|
||||
return g, features, labels, train_mask, test_mask
|
||||
|
||||
|
||||
###############################################################################
|
||||
# When a model is trained, we can use the following method to evaluate
|
||||
# the performance of the model on the test dataset:
|
||||
|
||||
|
||||
def evaluate(model, g, features, labels, mask):
|
||||
model.eval()
|
||||
with th.no_grad():
|
||||
logits = model(g, features)
|
||||
logits = logits[mask]
|
||||
labels = labels[mask]
|
||||
_, indices = th.max(logits, dim=1)
|
||||
correct = th.sum(indices == labels)
|
||||
return correct.item() * 1.0 / len(labels)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# We then train the network as follows:
|
||||
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
g, features, labels, train_mask, test_mask = load_cora_data()
|
||||
# Add edges between each node and itself to preserve old node representations
|
||||
g.add_edges(g.nodes(), g.nodes())
|
||||
optimizer = th.optim.Adam(net.parameters(), lr=1e-2)
|
||||
dur = []
|
||||
for epoch in range(50):
|
||||
if epoch >= 3:
|
||||
t0 = time.time()
|
||||
net.train()
|
||||
logits = net(g, features)
|
||||
logp = F.log_softmax(logits, 1)
|
||||
loss = F.nll_loss(logp[train_mask], labels[train_mask])
|
||||
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
if epoch >= 3:
|
||||
dur.append(time.time() - t0)
|
||||
acc = evaluate(net, g, features, labels, test_mask)
|
||||
print(
|
||||
"Epoch {:05d} | Loss {:.4f} | Test Acc {:.4f} | Time(s) {:.4f}".format(
|
||||
epoch, loss.item(), acc, np.mean(dur)
|
||||
)
|
||||
)
|
||||
###############################################################################
|
||||
# .. _math:
|
||||
#
|
||||
# GCN in one formula
|
||||
# ------------------
|
||||
# Mathematically, the GCN model follows this formula:
|
||||
#
|
||||
# :math:`H^{(l+1)} = \sigma(\tilde{D}^{-\frac{1}{2}}\tilde{A}\tilde{D}^{-\frac{1}{2}}H^{(l)}W^{(l)})`
|
||||
#
|
||||
# Here, :math:`H^{(l)}` denotes the :math:`l^{th}` layer in the network,
|
||||
# :math:`\sigma` is the non-linearity, and :math:`W` is the weight matrix for
|
||||
# this layer. :math:`\tilde{D}` and :math:`\tilde{A}` are separately the degree
|
||||
# and adjacency matrices for the graph. With the superscript ~, we are referring
|
||||
# to the variant where we add additional edges between each node and itself to
|
||||
# preserve its old representation in graph convolutions. The shape of the input
|
||||
# :math:`H^{(0)}` is :math:`N \times D`, where :math:`N` is the number of nodes
|
||||
# and :math:`D` is the number of input features. We can chain up multiple
|
||||
# layers as such to produce a node-level representation output with shape
|
||||
# :math:`N \times F`, where :math:`F` is the dimension of the output node
|
||||
# feature vector.
|
||||
#
|
||||
# The equation can be efficiently implemented using sparse matrix
|
||||
# multiplication kernels (such as Kipf's
|
||||
# `pygcn <https://github.com/tkipf/pygcn>`_ code). The above DGL implementation
|
||||
# in fact has already used this trick due to the use of builtin functions.
|
||||
#
|
||||
# Note that the tutorial code implements a simplified version of GCN where we
|
||||
# replace :math:`\tilde{D}^{-\frac{1}{2}}\tilde{A}\tilde{D}^{-\frac{1}{2}}` with
|
||||
# :math:`\tilde{A}`. For a full implementation, see our example
|
||||
# `here <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gcn>`_.
|
||||
@@ -0,0 +1,417 @@
|
||||
"""
|
||||
.. _model-rgcn:
|
||||
|
||||
Relational Graph Convolutional Network
|
||||
================================================
|
||||
|
||||
**Author:** Lingfan Yu, Mufei Li, Zheng Zhang
|
||||
|
||||
.. warning::
|
||||
|
||||
The tutorial aims at gaining insights into the paper, with code as a mean
|
||||
of explanation. The implementation thus is NOT optimized for running
|
||||
efficiency. For recommended implementation, please refer to the `official
|
||||
examples <https://github.com/dmlc/dgl/tree/master/examples>`_.
|
||||
|
||||
In this tutorial, you learn how to implement a relational graph convolutional
|
||||
network (R-GCN). This type of network is one effort to generalize GCN
|
||||
to handle different relationships between entities in a knowledge base. To
|
||||
learn more about the research behind R-GCN, see `Modeling Relational Data with Graph Convolutional
|
||||
Networks <https://arxiv.org/pdf/1703.06103.pdf>`_
|
||||
|
||||
The straightforward graph convolutional network (GCN) exploits
|
||||
structural information of a dataset (that is, the graph connectivity) in order to
|
||||
improve the extraction of node representations. Graph edges are left as
|
||||
untyped.
|
||||
|
||||
A knowledge graph is made up of a collection of triples in the form
|
||||
subject, relation, object. Edges thus encode important information and
|
||||
have their own embeddings to be learned. Furthermore, there may exist
|
||||
multiple edges among any given pair.
|
||||
|
||||
"""
|
||||
###############################################################################
|
||||
# A brief introduction to R-GCN
|
||||
# ---------------------------
|
||||
# In *statistical relational learning* (SRL), there are two fundamental
|
||||
# tasks:
|
||||
#
|
||||
# - **Entity classification** - Where you assign types and categorical
|
||||
# properties to entities.
|
||||
# - **Link prediction** - Where you recover missing triples.
|
||||
#
|
||||
# In both cases, missing information is expected to be recovered from the
|
||||
# neighborhood structure of the graph. For example, the R-GCN
|
||||
# paper cited earlier provides the following example. Knowing that Mikhail Baryshnikov was educated at the Vaganova Academy
|
||||
# implies both that Mikhail Baryshnikov should have the label person, and
|
||||
# that the triple (Mikhail Baryshnikov, lived in, Russia) must belong to the
|
||||
# knowledge graph.
|
||||
#
|
||||
# R-GCN solves these two problems using a common graph convolutional network. It's
|
||||
# extended with multi-edge encoding to compute embedding of the entities, but
|
||||
# with different downstream processing.
|
||||
#
|
||||
# - Entity classification is done by attaching a softmax classifier at the
|
||||
# final embedding of an entity (node). Training is through loss of standard
|
||||
# cross-entropy.
|
||||
# - Link prediction is done by reconstructing an edge with an autoencoder
|
||||
# architecture, using a parameterized score function. Training uses negative
|
||||
# sampling.
|
||||
#
|
||||
# This tutorial focuses on the first task, entity classification, to show how to generate entity
|
||||
# representation. `Complete
|
||||
# code <https://github.com/dmlc/dgl/tree/master/examples/pytorch/rgcn>`_
|
||||
# for both tasks is found in the DGL Github repository.
|
||||
#
|
||||
# Key ideas of R-GCN
|
||||
# -------------------
|
||||
# Recall that in GCN, the hidden representation for each node :math:`i` at
|
||||
# :math:`(l+1)^{th}` layer is computed by:
|
||||
#
|
||||
# .. math:: h_i^{l+1} = \sigma\left(\sum_{j\in N_i}\frac{1}{c_i} W^{(l)} h_j^{(l)}\right)~~~~~~~~~~(1)\\
|
||||
#
|
||||
# where :math:`c_i` is a normalization constant.
|
||||
#
|
||||
# The key difference between R-GCN and GCN is that in R-GCN, edges can
|
||||
# represent different relations. In GCN, weight :math:`W^{(l)}` in equation
|
||||
# :math:`(1)` is shared by all edges in layer :math:`l`. In contrast, in
|
||||
# R-GCN, different edge types use different weights and only edges of the
|
||||
# same relation type :math:`r` are associated with the same projection weight
|
||||
# :math:`W_r^{(l)}`.
|
||||
#
|
||||
# So the hidden representation of entities in :math:`(l+1)^{th}` layer in
|
||||
# R-GCN can be formulated as the following equation:
|
||||
#
|
||||
# .. math:: h_i^{l+1} = \sigma\left(W_0^{(l)}h_i^{(l)}+\sum_{r\in R}\sum_{j\in N_i^r}\frac{1}{c_{i,r}}W_r^{(l)}h_j^{(l)}\right)~~~~~~~~~~(2)\\
|
||||
#
|
||||
# where :math:`N_i^r` denotes the set of neighbor indices of node :math:`i`
|
||||
# under relation :math:`r\in R` and :math:`c_{i,r}` is a normalization
|
||||
# constant. In entity classification, the R-GCN paper uses
|
||||
# :math:`c_{i,r}=|N_i^r|`.
|
||||
#
|
||||
# The problem of applying the above equation directly is the rapid growth of
|
||||
# the number of parameters, especially with highly multi-relational data. In
|
||||
# order to reduce model parameter size and prevent overfitting, the original
|
||||
# paper proposes to use basis decomposition.
|
||||
#
|
||||
# .. math:: W_r^{(l)}=\sum\limits_{b=1}^B a_{rb}^{(l)}V_b^{(l)}~~~~~~~~~~(3)\\
|
||||
#
|
||||
# Therefore, the weight :math:`W_r^{(l)}` is a linear combination of basis
|
||||
# transformation :math:`V_b^{(l)}` with coefficients :math:`a_{rb}^{(l)}`.
|
||||
# The number of bases :math:`B` is much smaller than the number of relations
|
||||
# in the knowledge base.
|
||||
#
|
||||
# .. note::
|
||||
# Another weight regularization, block-decomposition, is implemented in
|
||||
# the `link prediction <link-prediction_>`_.
|
||||
#
|
||||
# Implement R-GCN in DGL
|
||||
# ----------------------
|
||||
#
|
||||
# An R-GCN model is composed of several R-GCN layers. The first R-GCN layer
|
||||
# also serves as input layer and takes in features (for example, description texts)
|
||||
# that are associated with node entity and project to hidden space. In this tutorial,
|
||||
# we only use the entity ID as an entity feature.
|
||||
#
|
||||
# R-GCN layers
|
||||
# ~~~~~~~~~~~~
|
||||
#
|
||||
# For each node, an R-GCN layer performs the following steps:
|
||||
#
|
||||
# - Compute outgoing message using node representation and weight matrix
|
||||
# associated with the edge type (message function)
|
||||
# - Aggregate incoming messages and generate new node representations (reduce
|
||||
# and apply function)
|
||||
#
|
||||
# The following code is the definition of an R-GCN hidden layer.
|
||||
#
|
||||
# .. note::
|
||||
# Each relation type is associated with a different weight. Therefore,
|
||||
# the full weight matrix has three dimensions: relation, input_feature,
|
||||
# output_feature.
|
||||
#
|
||||
# .. note::
|
||||
#
|
||||
# This is showing how to implement an R-GCN from scratch. DGL provides a more
|
||||
# efficient :class:`builtin R-GCN layer module <dgl.nn.pytorch.conv.RelGraphConv>`.
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
os.environ["DGLBACKEND"] = "pytorch"
|
||||
from functools import partial
|
||||
|
||||
import dgl
|
||||
import dgl.function as fn
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from dgl import DGLGraph
|
||||
|
||||
|
||||
class RGCNLayer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_feat,
|
||||
out_feat,
|
||||
num_rels,
|
||||
num_bases=-1,
|
||||
bias=None,
|
||||
activation=None,
|
||||
is_input_layer=False,
|
||||
):
|
||||
super(RGCNLayer, self).__init__()
|
||||
self.in_feat = in_feat
|
||||
self.out_feat = out_feat
|
||||
self.num_rels = num_rels
|
||||
self.num_bases = num_bases
|
||||
self.bias = bias
|
||||
self.activation = activation
|
||||
self.is_input_layer = is_input_layer
|
||||
|
||||
# sanity check
|
||||
if self.num_bases <= 0 or self.num_bases > self.num_rels:
|
||||
self.num_bases = self.num_rels
|
||||
# weight bases in equation (3)
|
||||
self.weight = nn.Parameter(
|
||||
torch.Tensor(self.num_bases, self.in_feat, self.out_feat)
|
||||
)
|
||||
if self.num_bases < self.num_rels:
|
||||
# linear combination coefficients in equation (3)
|
||||
self.w_comp = nn.Parameter(
|
||||
torch.Tensor(self.num_rels, self.num_bases)
|
||||
)
|
||||
# add bias
|
||||
if self.bias:
|
||||
self.bias = nn.Parameter(torch.Tensor(out_feat))
|
||||
# init trainable parameters
|
||||
nn.init.xavier_uniform_(
|
||||
self.weight, gain=nn.init.calculate_gain("relu")
|
||||
)
|
||||
if self.num_bases < self.num_rels:
|
||||
nn.init.xavier_uniform_(
|
||||
self.w_comp, gain=nn.init.calculate_gain("relu")
|
||||
)
|
||||
if self.bias:
|
||||
nn.init.xavier_uniform_(
|
||||
self.bias, gain=nn.init.calculate_gain("relu")
|
||||
)
|
||||
|
||||
def forward(self, g):
|
||||
if self.num_bases < self.num_rels:
|
||||
# generate all weights from bases (equation (3))
|
||||
weight = self.weight.view(
|
||||
self.in_feat, self.num_bases, self.out_feat
|
||||
)
|
||||
weight = torch.matmul(self.w_comp, weight).view(
|
||||
self.num_rels, self.in_feat, self.out_feat
|
||||
)
|
||||
else:
|
||||
weight = self.weight
|
||||
if self.is_input_layer:
|
||||
|
||||
def message_func(edges):
|
||||
# for input layer, matrix multiply can be converted to be
|
||||
# an embedding lookup using source node id
|
||||
embed = weight.view(-1, self.out_feat)
|
||||
index = edges.data[dgl.ETYPE] * self.in_feat + edges.src["id"]
|
||||
return {"msg": embed[index] * edges.data["norm"]}
|
||||
|
||||
else:
|
||||
|
||||
def message_func(edges):
|
||||
w = weight[edges.data[dgl.ETYPE]]
|
||||
msg = torch.bmm(edges.src["h"].unsqueeze(1), w).squeeze()
|
||||
msg = msg * edges.data["norm"]
|
||||
return {"msg": msg}
|
||||
|
||||
def apply_func(nodes):
|
||||
h = nodes.data["h"]
|
||||
if self.bias:
|
||||
h = h + self.bias
|
||||
if self.activation:
|
||||
h = self.activation(h)
|
||||
return {"h": h}
|
||||
|
||||
g.update_all(message_func, fn.sum(msg="msg", out="h"), apply_func)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Full R-GCN model defined
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
num_nodes,
|
||||
h_dim,
|
||||
out_dim,
|
||||
num_rels,
|
||||
num_bases=-1,
|
||||
num_hidden_layers=1,
|
||||
):
|
||||
super(Model, 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 = num_bases
|
||||
self.num_hidden_layers = num_hidden_layers
|
||||
|
||||
# create rgcn layers
|
||||
self.build_model()
|
||||
|
||||
# create initial features
|
||||
self.features = self.create_features()
|
||||
|
||||
def build_model(self):
|
||||
self.layers = nn.ModuleList()
|
||||
# input to hidden
|
||||
i2h = self.build_input_layer()
|
||||
self.layers.append(i2h)
|
||||
# hidden to hidden
|
||||
for _ in range(self.num_hidden_layers):
|
||||
h2h = self.build_hidden_layer()
|
||||
self.layers.append(h2h)
|
||||
# hidden to output
|
||||
h2o = self.build_output_layer()
|
||||
self.layers.append(h2o)
|
||||
|
||||
# initialize feature for each node
|
||||
def create_features(self):
|
||||
features = torch.arange(self.num_nodes)
|
||||
return features
|
||||
|
||||
def build_input_layer(self):
|
||||
return RGCNLayer(
|
||||
self.num_nodes,
|
||||
self.h_dim,
|
||||
self.num_rels,
|
||||
self.num_bases,
|
||||
activation=F.relu,
|
||||
is_input_layer=True,
|
||||
)
|
||||
|
||||
def build_hidden_layer(self):
|
||||
return RGCNLayer(
|
||||
self.h_dim,
|
||||
self.h_dim,
|
||||
self.num_rels,
|
||||
self.num_bases,
|
||||
activation=F.relu,
|
||||
)
|
||||
|
||||
def build_output_layer(self):
|
||||
return RGCNLayer(
|
||||
self.h_dim,
|
||||
self.out_dim,
|
||||
self.num_rels,
|
||||
self.num_bases,
|
||||
activation=partial(F.softmax, dim=1),
|
||||
)
|
||||
|
||||
def forward(self, g):
|
||||
if self.features is not None:
|
||||
g.ndata["id"] = self.features
|
||||
for layer in self.layers:
|
||||
layer(g)
|
||||
return g.ndata.pop("h")
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Handle dataset
|
||||
# ~~~~~~~~~~~~~~~~
|
||||
# This tutorial uses Institute for Applied Informatics and Formal Description Methods (AIFB) dataset from R-GCN paper.
|
||||
|
||||
# load graph data
|
||||
dataset = dgl.data.rdf.AIFBDataset()
|
||||
g = dataset[0]
|
||||
category = dataset.predict_category
|
||||
train_mask = g.nodes[category].data.pop("train_mask")
|
||||
test_mask = g.nodes[category].data.pop("test_mask")
|
||||
train_idx = torch.nonzero(train_mask, as_tuple=False).squeeze()
|
||||
test_idx = torch.nonzero(test_mask, as_tuple=False).squeeze()
|
||||
labels = g.nodes[category].data.pop("label")
|
||||
num_rels = len(g.canonical_etypes)
|
||||
num_classes = dataset.num_classes
|
||||
# normalization factor
|
||||
for cetype in g.canonical_etypes:
|
||||
g.edges[cetype].data["norm"] = dgl.norm_by_dst(g, cetype).unsqueeze(1)
|
||||
category_id = g.ntypes.index(category)
|
||||
|
||||
###############################################################################
|
||||
# Create graph and model
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
# configurations
|
||||
n_hidden = 16 # number of hidden units
|
||||
n_bases = -1 # use number of relations as number of bases
|
||||
n_hidden_layers = 0 # use 1 input layer, 1 output layer, no hidden layer
|
||||
n_epochs = 25 # epochs to train
|
||||
lr = 0.01 # learning rate
|
||||
l2norm = 0 # L2 norm coefficient
|
||||
|
||||
# create graph
|
||||
g = dgl.to_homogeneous(g, edata=["norm"])
|
||||
node_ids = torch.arange(g.num_nodes())
|
||||
target_idx = node_ids[g.ndata[dgl.NTYPE] == category_id]
|
||||
|
||||
# create model
|
||||
model = Model(
|
||||
g.num_nodes(),
|
||||
n_hidden,
|
||||
num_classes,
|
||||
num_rels,
|
||||
num_bases=n_bases,
|
||||
num_hidden_layers=n_hidden_layers,
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# Training loop
|
||||
# ~~~~~~~~~~~~~~~~
|
||||
|
||||
# optimizer
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=l2norm)
|
||||
|
||||
print("start training...")
|
||||
model.train()
|
||||
for epoch in range(n_epochs):
|
||||
optimizer.zero_grad()
|
||||
logits = model.forward(g)
|
||||
logits = logits[target_idx]
|
||||
loss = F.cross_entropy(logits[train_idx], labels[train_idx])
|
||||
loss.backward()
|
||||
|
||||
optimizer.step()
|
||||
|
||||
train_acc = torch.sum(logits[train_idx].argmax(dim=1) == labels[train_idx])
|
||||
train_acc = train_acc.item() / len(train_idx)
|
||||
val_loss = F.cross_entropy(logits[test_idx], labels[test_idx])
|
||||
val_acc = torch.sum(logits[test_idx].argmax(dim=1) == labels[test_idx])
|
||||
val_acc = val_acc.item() / len(test_idx)
|
||||
print(
|
||||
"Epoch {:05d} | ".format(epoch)
|
||||
+ "Train Accuracy: {:.4f} | Train Loss: {:.4f} | ".format(
|
||||
train_acc, loss.item()
|
||||
)
|
||||
+ "Validation Accuracy: {:.4f} | Validation loss: {:.4f}".format(
|
||||
val_acc, val_loss.item()
|
||||
)
|
||||
)
|
||||
###############################################################################
|
||||
# .. _link-prediction:
|
||||
#
|
||||
# The second task, link prediction
|
||||
# --------------------------------
|
||||
# So far, you have seen how to use DGL to implement entity classification with an
|
||||
# R-GCN model. In the knowledge base setting, representation generated by
|
||||
# R-GCN can be used to uncover potential relationships between nodes. In the
|
||||
# R-GCN paper, the authors feed the entity representations generated by R-GCN
|
||||
# into the `DistMult <https://arxiv.org/pdf/1412.6575.pdf>`_ prediction model
|
||||
# to predict possible relationships.
|
||||
#
|
||||
# The implementation is similar to that presented here, but with an extra DistMult layer
|
||||
# stacked on top of the R-GCN layers. You can find the complete
|
||||
# implementation of link prediction with R-GCN in our `Github Python code
|
||||
# example <https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn/link.py>`_.
|
||||
@@ -0,0 +1,653 @@
|
||||
"""
|
||||
.. _model-line-graph:
|
||||
|
||||
Line Graph Neural Network
|
||||
=========================
|
||||
|
||||
**Author**: `Qi Huang <https://github.com/HQ01>`_, Yu Gai,
|
||||
`Minjie Wang <https://jermainewang.github.io/>`_, Zheng Zhang
|
||||
|
||||
.. warning::
|
||||
|
||||
The tutorial aims at gaining insights into the paper, with code as a mean
|
||||
of explanation. The implementation thus is NOT optimized for running
|
||||
efficiency. For recommended implementation, please refer to the `official
|
||||
examples <https://github.com/dmlc/dgl/tree/master/examples>`_.
|
||||
|
||||
"""
|
||||
|
||||
###########################################################################################
|
||||
#
|
||||
# In this tutorial, you learn how to solve community detection tasks by implementing a line
|
||||
# graph neural network (LGNN). Community detection, or graph clustering, consists of partitioning
|
||||
# the vertices in a graph into clusters in which nodes are more similar to
|
||||
# one another.
|
||||
#
|
||||
# In the :doc:`Graph convolutinal network tutorial <1_gcn>`, you learned how to classify the nodes of an input
|
||||
# graph in a semi-supervised setting. You used a graph convolutional neural network (GCN)
|
||||
# as an embedding mechanism for graph features.
|
||||
#
|
||||
# To generalize a graph neural network (GNN) into supervised community detection, a line-graph based
|
||||
# variation of GNN is introduced in the research paper
|
||||
# `Supervised Community Detection with Line Graph Neural Networks <https://arxiv.org/abs/1705.08415>`__.
|
||||
# One of the highlights of the model is
|
||||
# to augment the straightforward GNN architecture so that it operates on
|
||||
# a line graph of edge adjacencies, defined with a non-backtracking operator.
|
||||
#
|
||||
# A line graph neural network (LGNN) shows how DGL can implement an advanced graph algorithm by
|
||||
# mixing basic tensor operations, sparse-matrix multiplication, and message-
|
||||
# passing APIs.
|
||||
#
|
||||
# In the following sections, you learn about community detection, line
|
||||
# graphs, LGNN, and its implementation.
|
||||
#
|
||||
# Supervised community detection task with the Cora dataset
|
||||
# --------------------------------------------
|
||||
# Community detection
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
# In a community detection task, you cluster similar nodes instead of
|
||||
# labeling them. The node similarity is typically described as having higher inner
|
||||
# density within each cluster.
|
||||
#
|
||||
# What's the difference between community detection and node classification?
|
||||
# Comparing to node classification, community detection focuses on retrieving
|
||||
# cluster information in the graph, rather than assigning a specific label to
|
||||
# a node. For example, as long as a node is clustered with its community
|
||||
# members, it doesn't matter whether the node is assigned as "community A",
|
||||
# or "community B", while assigning all "great movies" to label "bad movies"
|
||||
# will be a disaster in a movie network classification task.
|
||||
#
|
||||
# What's the difference then, between a community detection algorithm and
|
||||
# other clustering algorithm such as k-means? Community detection algorithm operates on
|
||||
# graph-structured data. Comparing to k-means, community detection leverages
|
||||
# graph structure, instead of simply clustering nodes based on their
|
||||
# features.
|
||||
#
|
||||
# Cora dataset
|
||||
# ~~~~~
|
||||
# To be consistent with the GCN tutorial,
|
||||
# you use the `Cora dataset <https://linqs.soe.ucsc.edu/data>`__
|
||||
# to illustrate a simple community detection task. Cora is a scientific publication dataset,
|
||||
# with 2708 papers belonging to seven
|
||||
# different machine learning fields. Here, you formulate Cora as a
|
||||
# directed graph, with each node being a paper, and each edge being a
|
||||
# citation link (A->B means A cites B). Here is a visualization of the whole
|
||||
# Cora dataset.
|
||||
#
|
||||
# .. figure:: https://i.imgur.com/X404Byc.png
|
||||
# :alt: cora
|
||||
# :height: 400px
|
||||
# :width: 500px
|
||||
# :align: center
|
||||
#
|
||||
# Cora naturally contains seven classes, and statistics below show that each
|
||||
# class does satisfy our assumption of community, i.e. nodes of same class
|
||||
# class have higher connection probability among them than with nodes of different class.
|
||||
# The following code snippet verifies that there are more intra-class edges
|
||||
# than inter-class.
|
||||
|
||||
import os
|
||||
|
||||
os.environ["DGLBACKEND"] = "pytorch"
|
||||
import dgl
|
||||
import torch
|
||||
import torch as th
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from dgl.data import citation_graph as citegrh
|
||||
|
||||
data = citegrh.load_cora()
|
||||
|
||||
G = data[0]
|
||||
labels = th.tensor(G.ndata["label"])
|
||||
|
||||
# find all the nodes labeled with class 0
|
||||
label0_nodes = th.nonzero(labels == 0, as_tuple=False).squeeze()
|
||||
# find all the edges pointing to class 0 nodes
|
||||
src, _ = G.in_edges(label0_nodes)
|
||||
src_labels = labels[src]
|
||||
# find all the edges whose both endpoints are in class 0
|
||||
intra_src = th.nonzero(src_labels == 0, as_tuple=False)
|
||||
print("Intra-class edges percent: %.4f" % (len(intra_src) / len(src_labels)))
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
###########################################################################################
|
||||
# Binary community subgraph from Cora with a test dataset
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# Without loss of generality, in this tutorial you limit the scope of the
|
||||
# task to binary community detection.
|
||||
#
|
||||
# .. note::
|
||||
#
|
||||
# To create a practice binary-community dataset from Cora, first extract
|
||||
# all two-class pairs from the original Cora seven classes. For each pair, you
|
||||
# treat each class as one community, and find the largest subgraph that
|
||||
# at least contains one cross-community edge as the training example. As
|
||||
# a result, there are a total of 21 training samples in this small dataset.
|
||||
#
|
||||
# With the following code, you can visualize one of the training samples and its community structure.
|
||||
|
||||
import networkx as nx
|
||||
|
||||
train_set = dgl.data.CoraBinary()
|
||||
G1, pmpd1, label1 = train_set[1]
|
||||
nx_G1 = G1.to_networkx()
|
||||
|
||||
|
||||
def visualize(labels, g):
|
||||
pos = nx.spring_layout(g, seed=1)
|
||||
plt.figure(figsize=(8, 8))
|
||||
plt.axis("off")
|
||||
nx.draw_networkx(
|
||||
g,
|
||||
pos=pos,
|
||||
node_size=50,
|
||||
cmap=plt.get_cmap("coolwarm"),
|
||||
node_color=labels,
|
||||
edge_color="k",
|
||||
arrows=False,
|
||||
width=0.5,
|
||||
style="dotted",
|
||||
with_labels=False,
|
||||
)
|
||||
|
||||
|
||||
visualize(label1, nx_G1)
|
||||
|
||||
###########################################################################################
|
||||
# To learn more, go the original research paper to see how to generalize
|
||||
# to multiple communities case.
|
||||
#
|
||||
# Community detection in a supervised setting
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# The community detection problem could be tackled with both supervised and
|
||||
# unsupervised approaches. You can formulate
|
||||
# community detection in a supervised setting as follows:
|
||||
#
|
||||
# - Each training example consists of :math:`(G, L)`, where :math:`G` is a
|
||||
# directed graph :math:`(V, E)`. For each node :math:`v` in :math:`V`, we
|
||||
# assign a ground truth community label :math:`z_v \in \{0,1\}`.
|
||||
# - The parameterized model :math:`f(G, \theta)` predicts a label set
|
||||
# :math:`\tilde{Z} = f(G)` for nodes :math:`V`.
|
||||
# - For each example :math:`(G,L)`, the model learns to minimize a specially
|
||||
# designed loss function (equivariant loss) :math:`L_{equivariant} =
|
||||
# (\tilde{Z},Z)`
|
||||
#
|
||||
# .. note::
|
||||
#
|
||||
# In this supervised setting, the model naturally predicts a label for
|
||||
# each community. However, community assignment should be equivariant to
|
||||
# label permutations. To achieve this, in each forward process, we take
|
||||
# the minimum among losses calculated from all possible permutations of
|
||||
# labels.
|
||||
#
|
||||
# Mathematically, this means
|
||||
# :math:`L_{equivariant} = \underset{\pi \in S_c} {min}-\log(\hat{\pi}, \pi)`,
|
||||
# where :math:`S_c` is the set of all permutations of labels, and
|
||||
# :math:`\hat{\pi}` is the set of predicted labels,
|
||||
# :math:`- \log(\hat{\pi},\pi)` denotes negative log likelihood.
|
||||
#
|
||||
# For instance, for a sample graph with node :math:`\{1,2,3,4\}` and
|
||||
# community assignment :math:`\{A, A, A, B\}`, with each node's label
|
||||
# :math:`l \in \{0,1\}`,The group of all possible permutations
|
||||
# :math:`S_c = \{\{0,0,0,1\}, \{1,1,1,0\}\}`.
|
||||
#
|
||||
# Line graph neural network key ideas
|
||||
# ------------------------------------
|
||||
# An key innovation in this topic is the use of a line graph.
|
||||
# Unlike models in previous tutorials, message passing happens not only on the
|
||||
# original graph, e.g. the binary community subgraph from Cora, but also on the
|
||||
# line graph associated with the original graph.
|
||||
#
|
||||
# What is a line-graph?
|
||||
# ~~~~~~~~~~~~~~~~~~~~~
|
||||
# In graph theory, line graph is a graph representation that encodes the
|
||||
# edge adjacency structure in the original graph.
|
||||
#
|
||||
# Specifically, a line-graph :math:`L(G)` turns an edge of the original graph `G`
|
||||
# into a node. This is illustrated with the graph below (taken from the
|
||||
# research paper).
|
||||
#
|
||||
# .. figure:: https://i.imgur.com/4WO5jEm.png
|
||||
# :alt: lg
|
||||
# :align: center
|
||||
#
|
||||
# Here, :math:`e_{A}:= (i\rightarrow j)` and :math:`e_{B}:= (j\rightarrow k)`
|
||||
# are two edges in the original graph :math:`G`. In line graph :math:`G_L`,
|
||||
# they correspond to nodes :math:`v^{l}_{A}, v^{l}_{B}`.
|
||||
#
|
||||
# The next natural question is, how to connect nodes in line-graph? How to
|
||||
# connect two edges? Here, we use the following connection rule:
|
||||
#
|
||||
# Two nodes :math:`v^{l}_{A}`, :math:`v^{l}_{B}` in `lg` are connected if
|
||||
# the corresponding two edges :math:`e_{A}, e_{B}` in `g` share one and only
|
||||
# one node:
|
||||
# :math:`e_{A}`'s destination node is :math:`e_{B}`'s source node
|
||||
# (:math:`j`).
|
||||
#
|
||||
# .. note::
|
||||
#
|
||||
# Mathematically, this definition corresponds to a notion called non-backtracking
|
||||
# operator:
|
||||
# :math:`B_{(i \rightarrow j), (\hat{i} \rightarrow \hat{j})}`
|
||||
# :math:`= \begin{cases}
|
||||
# 1 \text{ if } j = \hat{i}, \hat{j} \neq i\\
|
||||
# 0 \text{ otherwise} \end{cases}`
|
||||
# where an edge is formed if :math:`B_{node1, node2} = 1`.
|
||||
#
|
||||
#
|
||||
# One layer in LGNN, algorithm structure
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
#
|
||||
# LGNN chains together a series of line graph neural network layers. The graph
|
||||
# representation :math:`x` and its line graph companion :math:`y` evolve with
|
||||
# the dataflow as follows.
|
||||
#
|
||||
# .. figure:: https://i.imgur.com/bZGGIGp.png
|
||||
# :alt: alg
|
||||
# :align: center
|
||||
#
|
||||
# At the :math:`k`-th layer, the :math:`i`-th neuron of the :math:`l`-th
|
||||
# channel updates its embedding :math:`x^{(k+1)}_{i,l}` with:
|
||||
#
|
||||
# .. math::
|
||||
# \begin{split}
|
||||
# x^{(k+1)}_{i,l} ={}&\rho[x^{(k)}_{i}\theta^{(k)}_{1,l}
|
||||
# +(Dx^{(k)})_{i}\theta^{(k)}_{2,l} \\
|
||||
# &+\sum^{J-1}_{j=0}(A^{2^{j}}x^{k})_{i}\theta^{(k)}_{3+j,l}\\
|
||||
# &+[\{\text{Pm},\text{Pd}\}y^{(k)}]_{i}\theta^{(k)}_{3+J,l}] \\
|
||||
# &+\text{skip-connection}
|
||||
# \qquad i \in V, l = 1,2,3, ... b_{k+1}/2
|
||||
# \end{split}
|
||||
#
|
||||
# Then, the line-graph representation :math:`y^{(k+1)}_{i,l}` with,
|
||||
#
|
||||
# .. math::
|
||||
#
|
||||
# \begin{split}
|
||||
# y^{(k+1)}_{i',l^{'}} = {}&\rho[y^{(k)}_{i^{'}}\gamma^{(k)}_{1,l^{'}}+
|
||||
# (D_{L(G)}y^{(k)})_{i^{'}}\gamma^{(k)}_{2,l^{'}}\\
|
||||
# &+\sum^{J-1}_{j=0}(A_{L(G)}^{2^{j}}y^{k})_{i}\gamma^{(k)}_{3+j,l^{'}}\\
|
||||
# &+[\{\text{Pm},\text{Pd}\}^{T}x^{(k+1)}]_{i^{'}}\gamma^{(k)}_{3+J,l^{'}}]\\
|
||||
# &+\text{skip-connection}
|
||||
# \qquad i^{'} \in V_{l}, l^{'} = 1,2,3, ... b^{'}_{k+1}/2
|
||||
# \end{split}
|
||||
#
|
||||
# Where :math:`\text{skip-connection}` refers to performing the same operation without the non-linearity
|
||||
# :math:`\rho`, and with linear projection :math:`\theta_\{\frac{b_{k+1}}{2} + 1, ..., b_{k+1}-1, b_{k+1}\}`
|
||||
# and :math:`\gamma_\{\frac{b_{k+1}}{2} + 1, ..., b_{k+1}-1, b_{k+1}\}`.
|
||||
#
|
||||
# Implement LGNN in DGL
|
||||
# ---------------------
|
||||
# Even though the equations in the previous section might seem intimidating,
|
||||
# it helps to understand the following information before you implement the LGNN.
|
||||
#
|
||||
# The two equations are symmetric and can be implemented as two instances
|
||||
# of the same class with different parameters.
|
||||
# The first equation operates on graph representation :math:`x`,
|
||||
# whereas the second operates on line-graph
|
||||
# representation :math:`y`. Let us denote this abstraction as :math:`f`. Then
|
||||
# the first is :math:`f(x,y; \theta_x)`, and the second
|
||||
# is :math:`f(y,x, \theta_y)`. That is, they are parameterized to compute
|
||||
# representations of the original graph and its
|
||||
# companion line graph, respectively.
|
||||
#
|
||||
# Each equation consists of four terms. Take the first one as an example, which follows.
|
||||
#
|
||||
# - :math:`x^{(k)}\theta^{(k)}_{1,l}`, a linear projection of previous
|
||||
# layer's output :math:`x^{(k)}`, denote as :math:`\text{prev}(x)`.
|
||||
# - :math:`(Dx^{(k)})\theta^{(k)}_{2,l}`, a linear projection of degree
|
||||
# operator on :math:`x^{(k)}`, denote as :math:`\text{deg}(x)`.
|
||||
# - :math:`\sum^{J-1}_{j=0}(A^{2^{j}}x^{(k)})\theta^{(k)}_{3+j,l}`,
|
||||
# a summation of :math:`2^{j}` adjacency operator on :math:`x^{(k)}`,
|
||||
# denote as :math:`\text{radius}(x)`
|
||||
# - :math:`[\{Pm,Pd\}y^{(k)}]\theta^{(k)}_{3+J,l}`, fusing another
|
||||
# graph's embedding information using incidence matrix
|
||||
# :math:`\{Pm, Pd\}`, followed with a linear projection,
|
||||
# denote as :math:`\text{fuse}(y)`.
|
||||
#
|
||||
# Each of the terms are performed again with different
|
||||
# parameters, and without the nonlinearity after the sum.
|
||||
# Therefore, :math:`f` could be written as:
|
||||
#
|
||||
# .. math::
|
||||
# \begin{split}
|
||||
# f(x^{(k)},y^{(k)}) = {}\rho[&\text{prev}(x^{(k-1)}) + \text{deg}(x^{(k-1)}) +\text{radius}(x^{k-1})
|
||||
# +\text{fuse}(y^{(k)})]\\
|
||||
# +&\text{prev}(x^{(k-1)}) + \text{deg}(x^{(k-1)}) +\text{radius}(x^{k-1}) +\text{fuse}(y^{(k)})
|
||||
# \end{split}
|
||||
#
|
||||
# Two equations are chained-up in the following order:
|
||||
#
|
||||
# .. math::
|
||||
# \begin{split}
|
||||
# x^{(k+1)} = {}& f(x^{(k)}, y^{(k)})\\
|
||||
# y^{(k+1)} = {}& f(y^{(k)}, x^{(k+1)})
|
||||
# \end{split}
|
||||
#
|
||||
# Keep in mind the listed observations in this overview and proceed to implementation.
|
||||
# An important point is that you use different strategies for the noted terms.
|
||||
#
|
||||
# .. note::
|
||||
# You can understand :math:`\{Pm, Pd\}` more thoroughly with this explanation.
|
||||
# Roughly speaking, there is a relationship between how :math:`g` and
|
||||
# :math:`lg` (the line graph) work together with loopy brief propagation.
|
||||
# Here, you implement :math:`\{Pm, Pd\}` as a SciPy COO sparse matrix in the dataset,
|
||||
# and stack them as tensors when batching. Another batching solution is to
|
||||
# treat :math:`\{Pm, Pd\}` as the adjacency matrix of a bipartite graph, which maps
|
||||
# line graph's feature to graph's, and vice versa.
|
||||
#
|
||||
# Implementing :math:`\text{prev}` and :math:`\text{deg}` as tensor operation
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# Linear projection and degree operation are both simply matrix
|
||||
# multiplication. Write them as PyTorch tensor operations.
|
||||
#
|
||||
# In ``__init__``, you define the projection variables.
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# self.linear_prev = nn.Linear(in_feats, out_feats)
|
||||
# self.linear_deg = nn.Linear(in_feats, out_feats)
|
||||
#
|
||||
#
|
||||
# In ``forward()``, :math:`\text{prev}` and :math:`\text{deg}` are the same
|
||||
# as any other PyTorch tensor operations.
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# prev_proj = self.linear_prev(feat_a)
|
||||
# deg_proj = self.linear_deg(deg * feat_a)
|
||||
#
|
||||
# Implementing :math:`\text{radius}` as message passing in DGL
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# As discussed in GCN tutorial, you can formulate one adjacency operator as
|
||||
# doing one-step message passing. As a generalization, :math:`2^j` adjacency
|
||||
# operations can be formulated as performing :math:`2^j` step of message
|
||||
# passing. Therefore, the summation is equivalent to summing nodes'
|
||||
# representation of :math:`2^j, j=0, 1, 2..` step message passing, i.e.
|
||||
# gathering information in :math:`2^{j}` neighborhood of each node.
|
||||
#
|
||||
# In ``__init__``, define the projection variables used in each
|
||||
# :math:`2^j` steps of message passing.
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# self.linear_radius = nn.ModuleList(
|
||||
# [nn.Linear(in_feats, out_feats) for i in range(radius)])
|
||||
#
|
||||
# In ``__forward__``, use following function ``aggregate_radius()`` to
|
||||
# gather data from multiple hops. This can be seen in the following code.
|
||||
# Note that the ``update_all`` is called multiple times.
|
||||
|
||||
# Return a list containing features gathered from multiple radius.
|
||||
import dgl.function as fn
|
||||
|
||||
|
||||
def aggregate_radius(radius, g, z):
|
||||
# initializing list to collect message passing result
|
||||
z_list = []
|
||||
g.ndata["z"] = z
|
||||
# pulling message from 1-hop neighbourhood
|
||||
g.update_all(fn.copy_u(u="z", out="m"), fn.sum(msg="m", out="z"))
|
||||
z_list.append(g.ndata["z"])
|
||||
for i in range(radius - 1):
|
||||
for j in range(2**i):
|
||||
# pulling message from 2^j neighborhood
|
||||
g.update_all(fn.copy_u(u="z", out="m"), fn.sum(msg="m", out="z"))
|
||||
z_list.append(g.ndata["z"])
|
||||
return z_list
|
||||
|
||||
|
||||
#########################################################################
|
||||
# Implementing :math:`\text{fuse}` as sparse matrix multiplication
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# :math:`\{Pm, Pd\}` is a sparse matrix with only two non-zero entries on
|
||||
# each column. Therefore, you construct it as a sparse matrix in the dataset,
|
||||
# and implement :math:`\text{fuse}` as a sparse matrix multiplication.
|
||||
#
|
||||
# in ``__forward__``:
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# fuse = self.linear_fuse(th.mm(pm_pd, feat_b))
|
||||
#
|
||||
# Completing :math:`f(x, y)`
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# Finally, the following shows how to sum up all the terms together, pass it to skip connection, and
|
||||
# batch norm.
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# result = prev_proj + deg_proj + radius_proj + fuse
|
||||
#
|
||||
# Pass result to skip connection.
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# result = th.cat([result[:, :n], F.relu(result[:, n:])], 1)
|
||||
#
|
||||
# Then pass the result to batch norm.
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# result = self.bn(result) #Batch Normalization.
|
||||
#
|
||||
#
|
||||
# Here is the complete code for one LGNN layer's abstraction :math:`f(x,y)`
|
||||
class LGNNCore(nn.Module):
|
||||
def __init__(self, in_feats, out_feats, radius):
|
||||
super(LGNNCore, self).__init__()
|
||||
self.out_feats = out_feats
|
||||
self.radius = radius
|
||||
|
||||
self.linear_prev = nn.Linear(in_feats, out_feats)
|
||||
self.linear_deg = nn.Linear(in_feats, out_feats)
|
||||
self.linear_radius = nn.ModuleList(
|
||||
[nn.Linear(in_feats, out_feats) for i in range(radius)]
|
||||
)
|
||||
self.linear_fuse = nn.Linear(in_feats, out_feats)
|
||||
self.bn = nn.BatchNorm1d(out_feats)
|
||||
|
||||
def forward(self, g, feat_a, feat_b, deg, pm_pd):
|
||||
# term "prev"
|
||||
prev_proj = self.linear_prev(feat_a)
|
||||
# term "deg"
|
||||
deg_proj = self.linear_deg(deg * feat_a)
|
||||
|
||||
# term "radius"
|
||||
# aggregate 2^j-hop features
|
||||
hop2j_list = aggregate_radius(self.radius, g, feat_a)
|
||||
# apply linear transformation
|
||||
hop2j_list = [
|
||||
linear(x) for linear, x in zip(self.linear_radius, hop2j_list)
|
||||
]
|
||||
radius_proj = sum(hop2j_list)
|
||||
|
||||
# term "fuse"
|
||||
fuse = self.linear_fuse(th.mm(pm_pd, feat_b))
|
||||
|
||||
# sum them together
|
||||
result = prev_proj + deg_proj + radius_proj + fuse
|
||||
|
||||
# skip connection and batch norm
|
||||
n = self.out_feats // 2
|
||||
result = th.cat([result[:, :n], F.relu(result[:, n:])], 1)
|
||||
result = self.bn(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
##############################################################################################################
|
||||
# Chain-up LGNN abstractions as an LGNN layer
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
# To implement:
|
||||
#
|
||||
# .. math::
|
||||
# \begin{split}
|
||||
# x^{(k+1)} = {}& f(x^{(k)}, y^{(k)})\\
|
||||
# y^{(k+1)} = {}& f(y^{(k)}, x^{(k+1)})
|
||||
# \end{split}
|
||||
#
|
||||
# Chain-up two ``LGNNCore`` instances, as in the example code, with different parameters in the forward pass.
|
||||
class LGNNLayer(nn.Module):
|
||||
def __init__(self, in_feats, out_feats, radius):
|
||||
super(LGNNLayer, self).__init__()
|
||||
self.g_layer = LGNNCore(in_feats, out_feats, radius)
|
||||
self.lg_layer = LGNNCore(in_feats, out_feats, radius)
|
||||
|
||||
def forward(self, g, lg, x, lg_x, deg_g, deg_lg, pm_pd):
|
||||
next_x = self.g_layer(g, x, lg_x, deg_g, pm_pd)
|
||||
pm_pd_y = th.transpose(pm_pd, 0, 1)
|
||||
next_lg_x = self.lg_layer(lg, lg_x, x, deg_lg, pm_pd_y)
|
||||
return next_x, next_lg_x
|
||||
|
||||
|
||||
########################################################################################
|
||||
# Chain-up LGNN layers
|
||||
# ~~~~~~~~~~~~~~~~~~~~
|
||||
# Define an LGNN with three hidden layers, as in the following example.
|
||||
class LGNN(nn.Module):
|
||||
def __init__(self, radius):
|
||||
super(LGNN, self).__init__()
|
||||
self.layer1 = LGNNLayer(1, 16, radius) # input is scalar feature
|
||||
self.layer2 = LGNNLayer(16, 16, radius) # hidden size is 16
|
||||
self.layer3 = LGNNLayer(16, 16, radius)
|
||||
self.linear = nn.Linear(16, 2) # predice two classes
|
||||
|
||||
def forward(self, g, lg, pm_pd):
|
||||
# compute the degrees
|
||||
deg_g = g.in_degrees().float().unsqueeze(1)
|
||||
deg_lg = lg.in_degrees().float().unsqueeze(1)
|
||||
# use degree as the input feature
|
||||
x, lg_x = deg_g, deg_lg
|
||||
x, lg_x = self.layer1(g, lg, x, lg_x, deg_g, deg_lg, pm_pd)
|
||||
x, lg_x = self.layer2(g, lg, x, lg_x, deg_g, deg_lg, pm_pd)
|
||||
x, lg_x = self.layer3(g, lg, x, lg_x, deg_g, deg_lg, pm_pd)
|
||||
return self.linear(x)
|
||||
|
||||
|
||||
#########################################################################################
|
||||
# Training and inference
|
||||
# -----------------------
|
||||
# First load the data.
|
||||
from torch.utils.data import DataLoader
|
||||
|
||||
training_loader = DataLoader(
|
||||
train_set, batch_size=1, collate_fn=train_set.collate_fn, drop_last=True
|
||||
)
|
||||
|
||||
#######################################################################################
|
||||
# Next, define the main training loop. Note that each training sample contains
|
||||
# three objects: A :class:`~dgl.DGLGraph`, a SciPy sparse matrix ``pmpd``, and a label
|
||||
# array in ``numpy.ndarray``. Generate the line graph by using this command:
|
||||
#
|
||||
# ::
|
||||
#
|
||||
# lg = g.line_graph(backtracking=False)
|
||||
#
|
||||
# Note that ``backtracking=False`` is required to correctly simulate non-backtracking
|
||||
# operation. We also define a utility function to convert the SciPy sparse matrix to
|
||||
# torch sparse tensor.
|
||||
|
||||
# Create the model
|
||||
model = LGNN(radius=3)
|
||||
# define the optimizer
|
||||
optimizer = th.optim.Adam(model.parameters(), lr=1e-2)
|
||||
|
||||
# A utility function to convert a scipy.coo_matrix to torch.SparseFloat
|
||||
def sparse2th(mat):
|
||||
value = mat.data
|
||||
indices = th.LongTensor([mat.row, mat.col])
|
||||
tensor = th.sparse.FloatTensor(
|
||||
indices, th.from_numpy(value).float(), mat.shape
|
||||
)
|
||||
return tensor
|
||||
|
||||
|
||||
# Train for 20 epochs
|
||||
for i in range(20):
|
||||
all_loss = []
|
||||
all_acc = []
|
||||
for [g, pmpd, label] in training_loader:
|
||||
# Generate the line graph.
|
||||
lg = g.line_graph(backtracking=False)
|
||||
# Create torch tensors
|
||||
pmpd = sparse2th(pmpd)
|
||||
label = th.from_numpy(label)
|
||||
|
||||
# Forward
|
||||
z = model(g, lg, pmpd)
|
||||
|
||||
# Calculate loss:
|
||||
# Since there are only two communities, there are only two permutations
|
||||
# of the community labels.
|
||||
loss_perm1 = F.cross_entropy(z, label)
|
||||
loss_perm2 = F.cross_entropy(z, 1 - label)
|
||||
loss = th.min(loss_perm1, loss_perm2)
|
||||
|
||||
# Calculate accuracy:
|
||||
_, pred = th.max(z, 1)
|
||||
acc_perm1 = (pred == label).float().mean()
|
||||
acc_perm2 = (pred == 1 - label).float().mean()
|
||||
acc = th.max(acc_perm1, acc_perm2)
|
||||
all_loss.append(loss.item())
|
||||
all_acc.append(acc.item())
|
||||
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
niters = len(all_loss)
|
||||
print(
|
||||
"Epoch %d | loss %.4f | accuracy %.4f"
|
||||
% (i, sum(all_loss) / niters, sum(all_acc) / niters)
|
||||
)
|
||||
#######################################################################################
|
||||
# Visualize training progress
|
||||
# -----------------------------
|
||||
# You can visualize the network's community prediction on one training example,
|
||||
# together with the ground truth. Start this with the following code example.
|
||||
|
||||
pmpd1 = sparse2th(pmpd1)
|
||||
LG1 = G1.line_graph(backtracking=False)
|
||||
z = model(G1, LG1, pmpd1)
|
||||
_, pred = th.max(z, 1)
|
||||
visualize(pred, nx_G1)
|
||||
|
||||
#######################################################################################
|
||||
# Compared with the ground truth. Note that the color might be reversed for the
|
||||
# two communities because the model is for correctly predicting the partitioning.
|
||||
visualize(label1, nx_G1)
|
||||
|
||||
#########################################
|
||||
# Here is an animation to better understand the process. (40 epochs)
|
||||
#
|
||||
# .. figure:: https://i.imgur.com/KDUyE1S.gif
|
||||
# :alt: lgnn-anim
|
||||
#
|
||||
# Batching graphs for parallelism
|
||||
# --------------------------------
|
||||
#
|
||||
# LGNN takes a collection of different graphs.
|
||||
# You might consider whether batching can be used for parallelism.
|
||||
#
|
||||
# Batching has been into the data loader itself.
|
||||
# In the ``collate_fn`` for PyTorch data loader, graphs are batched using DGL's
|
||||
# batched_graph API. DGL batches graphs by merging them
|
||||
# into a large graph, with each smaller graph's adjacency matrix being a block
|
||||
# along the diagonal of the large graph's adjacency matrix. Concatenate
|
||||
# :math`\{Pm,Pd\}` as block diagonal matrix in correspondence to DGL batched
|
||||
# graph API.
|
||||
|
||||
|
||||
def collate_fn(batch):
|
||||
graphs, pmpds, labels = zip(*batch)
|
||||
batched_graphs = dgl.batch(graphs)
|
||||
batched_pmpds = sp.block_diag(pmpds)
|
||||
batched_labels = np.concatenate(labels, axis=0)
|
||||
return batched_graphs, batched_pmpds, batched_labels
|
||||
|
||||
|
||||
######################################################################################
|
||||
# You can find the complete code on Github at
|
||||
# `Community Detection with Graph Neural Networks (CDGNN) <https://github.com/dmlc/dgl/tree/master/examples/pytorch/line_graph>`_.
|
||||
@@ -0,0 +1,545 @@
|
||||
"""
|
||||
.. _model-gat:
|
||||
|
||||
Understand Graph Attention Network
|
||||
=======================================
|
||||
|
||||
**Authors:** `Hao Zhang <https://github.com/sufeidechabei/>`_, `Mufei Li
|
||||
<https://github.com/mufeili>`_, `Minjie Wang
|
||||
<https://jermainewang.github.io/>`_ `Zheng Zhang
|
||||
<https://shanghai.nyu.edu/academics/faculty/directory/zheng-zhang>`_
|
||||
|
||||
.. warning::
|
||||
|
||||
The tutorial aims at gaining insights into the paper, with code as a mean
|
||||
of explanation. The implementation thus is NOT optimized for running
|
||||
efficiency. For recommended implementation, please refer to the `official
|
||||
examples <https://github.com/dmlc/dgl/tree/master/examples>`_.
|
||||
|
||||
In this tutorial, you learn about a graph attention network (GAT) and how it can be
|
||||
implemented in PyTorch. You can also learn to visualize and understand what the attention
|
||||
mechanism has learned.
|
||||
|
||||
The research described in the paper `Graph Convolutional Network (GCN) <https://arxiv.org/abs/1609.02907>`_,
|
||||
indicates that combining local graph structure and node-level features yields
|
||||
good performance on node classification tasks. However, the way GCN aggregates
|
||||
is structure-dependent, which can hurt its generalizability.
|
||||
|
||||
One workaround is to simply average over all neighbor node features as described in
|
||||
the research paper `GraphSAGE
|
||||
<https://www-cs-faculty.stanford.edu/people/jure/pubs/graphsage-nips17.pdf>`_.
|
||||
However, `Graph Attention Network <https://arxiv.org/abs/1710.10903>`_ proposes a
|
||||
different type of aggregation. GAT uses weighting neighbor features with feature dependent and
|
||||
structure-free normalization, in the style of attention.
|
||||
"""
|
||||
###############################################################
|
||||
# Introducing attention to GCN
|
||||
# ----------------------------
|
||||
#
|
||||
# The key difference between GAT and GCN is how the information from the one-hop neighborhood is aggregated.
|
||||
#
|
||||
# For GCN, a graph convolution operation produces the normalized sum of the node features of neighbors.
|
||||
#
|
||||
#
|
||||
# .. math::
|
||||
#
|
||||
# h_i^{(l+1)}=\sigma\left(\sum_{j\in \mathcal{N}(i)} {\frac{1}{c_{ij}} W^{(l)}h^{(l)}_j}\right)
|
||||
#
|
||||
#
|
||||
# where :math:`\mathcal{N}(i)` is the set of its one-hop neighbors (to include
|
||||
# :math:`v_i` in the set, simply add a self-loop to each node),
|
||||
# :math:`c_{ij}=\sqrt{|\mathcal{N}(i)|}\sqrt{|\mathcal{N}(j)|}` is a
|
||||
# normalization constant based on graph structure, :math:`\sigma` is an
|
||||
# activation function (GCN uses ReLU), and :math:`W^{(l)}` is a shared
|
||||
# weight matrix for node-wise feature transformation. Another model proposed in
|
||||
# `GraphSAGE
|
||||
# <https://www-cs-faculty.stanford.edu/people/jure/pubs/graphsage-nips17.pdf>`_
|
||||
# employs the same update rule except that they set
|
||||
# :math:`c_{ij}=|\mathcal{N}(i)|`.
|
||||
#
|
||||
# GAT introduces the attention mechanism as a substitute for the statically
|
||||
# normalized convolution operation. Below are the equations to compute the node
|
||||
# embedding :math:`h_i^{(l+1)}` of layer :math:`l+1` from the embeddings of
|
||||
# layer :math:`l`.
|
||||
#
|
||||
# .. image:: https://data.dgl.ai/tutorial/gat/gat.png
|
||||
# :width: 450px
|
||||
# :align: center
|
||||
#
|
||||
# .. math::
|
||||
#
|
||||
# \begin{align}
|
||||
# z_i^{(l)}&=W^{(l)}h_i^{(l)},&(1) \\
|
||||
# e_{ij}^{(l)}&=\text{LeakyReLU}(\vec a^{(l)^T}(z_i^{(l)}||z_j^{(l)})),&(2)\\
|
||||
# \alpha_{ij}^{(l)}&=\frac{\exp(e_{ij}^{(l)})}{\sum_{k\in \mathcal{N}(i)}^{}\exp(e_{ik}^{(l)})},&(3)\\
|
||||
# h_i^{(l+1)}&=\sigma\left(\sum_{j\in \mathcal{N}(i)} {\alpha^{(l)}_{ij} z^{(l)}_j }\right),&(4)
|
||||
# \end{align}
|
||||
#
|
||||
#
|
||||
# Explanations:
|
||||
#
|
||||
#
|
||||
# * Equation (1) is a linear transformation of the lower layer embedding :math:`h_i^{(l)}`
|
||||
# and :math:`W^{(l)}` is its learnable weight matrix.
|
||||
# * Equation (2) computes a pair-wise *un-normalized* attention score between two neighbors.
|
||||
# Here, it first concatenates the :math:`z` embeddings of the two nodes, where :math:`||`
|
||||
# denotes concatenation, then takes a dot product of it and a learnable weight vector
|
||||
# :math:`\vec a^{(l)}`, and applies a LeakyReLU in the end. This form of attention is
|
||||
# usually called *additive attention*, contrast with the dot-product attention in the
|
||||
# Transformer model.
|
||||
# * Equation (3) applies a softmax to normalize the attention scores on each node's
|
||||
# incoming edges.
|
||||
# * Equation (4) is similar to GCN. The embeddings from neighbors are aggregated together,
|
||||
# scaled by the attention scores.
|
||||
#
|
||||
# There are other details from the paper, such as dropout and skip connections.
|
||||
# For the purpose of simplicity, those details are left out of this tutorial. To see more details,
|
||||
# download the `full example <https://github.com/dmlc/dgl/blob/master/examples/pytorch/gat/gat.py>`_.
|
||||
# In its essence, GAT is just a different aggregation function with attention
|
||||
# over features of neighbors, instead of a simple mean aggregation.
|
||||
#
|
||||
# GAT in DGL
|
||||
# ----------
|
||||
#
|
||||
# DGL provides an off-the-shelf implementation of the GAT layer under the ``dgl.nn.<backend>``
|
||||
# subpackage. Simply import the ``GATConv`` as the follows.
|
||||
|
||||
import os
|
||||
|
||||
os.environ["DGLBACKEND"] = "pytorch"
|
||||
###############################################################
|
||||
# Readers can skip the following step-by-step explanation of the implementation and
|
||||
# jump to the `Put everything together`_ for training and visualization results.
|
||||
#
|
||||
# To begin, you can get an overall impression about how a ``GATLayer`` module is
|
||||
# implemented in DGL. In this section, the four equations above are broken down
|
||||
# one at a time.
|
||||
#
|
||||
# .. note::
|
||||
#
|
||||
# This is showing how to implement a GAT from scratch. DGL provides a more
|
||||
# efficient :class:`builtin GAT layer module <dgl.nn.pytorch.conv.GATConv>`.
|
||||
#
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from dgl.nn.pytorch import GATConv
|
||||
|
||||
|
||||
class GATLayer(nn.Module):
|
||||
def __init__(self, g, in_dim, out_dim):
|
||||
super(GATLayer, self).__init__()
|
||||
self.g = g
|
||||
# equation (1)
|
||||
self.fc = nn.Linear(in_dim, out_dim, bias=False)
|
||||
# equation (2)
|
||||
self.attn_fc = nn.Linear(2 * out_dim, 1, bias=False)
|
||||
self.reset_parameters()
|
||||
|
||||
def reset_parameters(self):
|
||||
"""Reinitialize learnable parameters."""
|
||||
gain = nn.init.calculate_gain("relu")
|
||||
nn.init.xavier_normal_(self.fc.weight, gain=gain)
|
||||
nn.init.xavier_normal_(self.attn_fc.weight, gain=gain)
|
||||
|
||||
def edge_attention(self, edges):
|
||||
# edge UDF for equation (2)
|
||||
z2 = torch.cat([edges.src["z"], edges.dst["z"]], dim=1)
|
||||
a = self.attn_fc(z2)
|
||||
return {"e": F.leaky_relu(a)}
|
||||
|
||||
def message_func(self, edges):
|
||||
# message UDF for equation (3) & (4)
|
||||
return {"z": edges.src["z"], "e": edges.data["e"]}
|
||||
|
||||
def reduce_func(self, nodes):
|
||||
# reduce UDF for equation (3) & (4)
|
||||
# equation (3)
|
||||
alpha = F.softmax(nodes.mailbox["e"], dim=1)
|
||||
# equation (4)
|
||||
h = torch.sum(alpha * nodes.mailbox["z"], dim=1)
|
||||
return {"h": h}
|
||||
|
||||
def forward(self, h):
|
||||
# equation (1)
|
||||
z = self.fc(h)
|
||||
self.g.ndata["z"] = z
|
||||
# equation (2)
|
||||
self.g.apply_edges(self.edge_attention)
|
||||
# equation (3) & (4)
|
||||
self.g.update_all(self.message_func, self.reduce_func)
|
||||
return self.g.ndata.pop("h")
|
||||
|
||||
|
||||
##################################################################
|
||||
# Equation (1)
|
||||
# ^^^^^^^^^^^^
|
||||
#
|
||||
# .. math::
|
||||
#
|
||||
# z_i^{(l)}=W^{(l)}h_i^{(l)},(1)
|
||||
#
|
||||
# The first one shows linear transformation. It's common and can be
|
||||
# easily implemented in Pytorch using ``torch.nn.Linear``.
|
||||
#
|
||||
# Equation (2)
|
||||
# ^^^^^^^^^^^^
|
||||
#
|
||||
# .. math::
|
||||
#
|
||||
# e_{ij}^{(l)}=\text{LeakyReLU}(\vec a^{(l)^T}(z_i^{(l)}|z_j^{(l)})),(2)
|
||||
#
|
||||
# The un-normalized attention score :math:`e_{ij}` is calculated using the
|
||||
# embeddings of adjacent nodes :math:`i` and :math:`j`. This suggests that the
|
||||
# attention scores can be viewed as edge data, which can be calculated by the
|
||||
# ``apply_edges`` API. The argument to the ``apply_edges`` is an **Edge UDF**,
|
||||
# which is defined as below:
|
||||
|
||||
|
||||
def edge_attention(self, edges):
|
||||
# edge UDF for equation (2)
|
||||
z2 = torch.cat([edges.src["z"], edges.dst["z"]], dim=1)
|
||||
a = self.attn_fc(z2)
|
||||
return {"e": F.leaky_relu(a)}
|
||||
|
||||
|
||||
########################################################################3
|
||||
# Here, the dot product with the learnable weight vector :math:`\vec{a^{(l)}}`
|
||||
# is implemented again using PyTorch's linear transformation ``attn_fc``. Note
|
||||
# that ``apply_edges`` will **batch** all the edge data in one tensor, so the
|
||||
# ``cat``, ``attn_fc`` here are applied on all the edges in parallel.
|
||||
#
|
||||
# Equation (3) & (4)
|
||||
# ^^^^^^^^^^^^^^^^^^
|
||||
#
|
||||
# .. math::
|
||||
#
|
||||
# \begin{align}
|
||||
# \alpha_{ij}^{(l)}&=\frac{\exp(e_{ij}^{(l)})}{\sum_{k\in \mathcal{N}(i)}^{}\exp(e_{ik}^{(l)})},&(3)\\
|
||||
# h_i^{(l+1)}&=\sigma\left(\sum_{j\in \mathcal{N}(i)} {\alpha^{(l)}_{ij} z^{(l)}_j }\right),&(4)
|
||||
# \end{align}
|
||||
#
|
||||
# Similar to GCN, ``update_all`` API is used to trigger message passing on all
|
||||
# the nodes. The message function sends out two tensors: the transformed ``z``
|
||||
# embedding of the source node and the un-normalized attention score ``e`` on
|
||||
# each edge. The reduce function then performs two tasks:
|
||||
#
|
||||
#
|
||||
# * Normalize the attention scores using softmax (equation (3)).
|
||||
# * Aggregate neighbor embeddings weighted by the attention scores (equation(4)).
|
||||
#
|
||||
# Both tasks first fetch data from the mailbox and then manipulate it on the
|
||||
# second dimension (``dim=1``), on which the messages are batched.
|
||||
|
||||
|
||||
def reduce_func(self, nodes):
|
||||
# reduce UDF for equation (3) & (4)
|
||||
# equation (3)
|
||||
alpha = F.softmax(nodes.mailbox["e"], dim=1)
|
||||
# equation (4)
|
||||
h = torch.sum(alpha * nodes.mailbox["z"], dim=1)
|
||||
return {"h": h}
|
||||
|
||||
|
||||
#####################################################################
|
||||
# Multi-head attention
|
||||
# ^^^^^^^^^^^^^^^^^^^^
|
||||
#
|
||||
# Analogous to multiple channels in ConvNet, GAT introduces **multi-head
|
||||
# attention** to enrich the model capacity and to stabilize the learning
|
||||
# process. Each attention head has its own parameters and their outputs can be
|
||||
# merged in two ways:
|
||||
#
|
||||
# .. math:: \text{concatenation}: h^{(l+1)}_{i} =||_{k=1}^{K}\sigma\left(\sum_{j\in \mathcal{N}(i)}\alpha_{ij}^{k}W^{k}h^{(l)}_{j}\right)
|
||||
#
|
||||
# or
|
||||
#
|
||||
# .. math:: \text{average}: h_{i}^{(l+1)}=\sigma\left(\frac{1}{K}\sum_{k=1}^{K}\sum_{j\in\mathcal{N}(i)}\alpha_{ij}^{k}W^{k}h^{(l)}_{j}\right)
|
||||
#
|
||||
# where :math:`K` is the number of heads. You can use
|
||||
# concatenation for intermediary layers and average for the final layer.
|
||||
#
|
||||
# Use the above defined single-head ``GATLayer`` as the building block
|
||||
# for the ``MultiHeadGATLayer`` below:
|
||||
|
||||
|
||||
class MultiHeadGATLayer(nn.Module):
|
||||
def __init__(self, g, in_dim, out_dim, num_heads, merge="cat"):
|
||||
super(MultiHeadGATLayer, self).__init__()
|
||||
self.heads = nn.ModuleList()
|
||||
for i in range(num_heads):
|
||||
self.heads.append(GATLayer(g, in_dim, out_dim))
|
||||
self.merge = merge
|
||||
|
||||
def forward(self, h):
|
||||
head_outs = [attn_head(h) for attn_head in self.heads]
|
||||
if self.merge == "cat":
|
||||
# concat on the output feature dimension (dim=1)
|
||||
return torch.cat(head_outs, dim=1)
|
||||
else:
|
||||
# merge using average
|
||||
return torch.mean(torch.stack(head_outs))
|
||||
|
||||
|
||||
###########################################################################
|
||||
# Put everything together
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
#
|
||||
# Now, you can define a two-layer GAT model.
|
||||
|
||||
|
||||
class GAT(nn.Module):
|
||||
def __init__(self, g, in_dim, hidden_dim, out_dim, num_heads):
|
||||
super(GAT, self).__init__()
|
||||
self.layer1 = MultiHeadGATLayer(g, in_dim, hidden_dim, num_heads)
|
||||
# Be aware that the input dimension is hidden_dim*num_heads since
|
||||
# multiple head outputs are concatenated together. Also, only
|
||||
# one attention head in the output layer.
|
||||
self.layer2 = MultiHeadGATLayer(g, hidden_dim * num_heads, out_dim, 1)
|
||||
|
||||
def forward(self, h):
|
||||
h = self.layer1(h)
|
||||
h = F.elu(h)
|
||||
h = self.layer2(h)
|
||||
return h
|
||||
|
||||
|
||||
import networkx as nx
|
||||
|
||||
#############################################################################
|
||||
# We then load the Cora dataset using DGL's built-in data module.
|
||||
|
||||
from dgl import DGLGraph
|
||||
from dgl.data import citation_graph as citegrh
|
||||
|
||||
|
||||
def load_cora_data():
|
||||
data = citegrh.load_cora()
|
||||
g = data[0]
|
||||
mask = torch.BoolTensor(g.ndata["train_mask"])
|
||||
return g, g.ndata["feat"], g.ndata["label"], mask
|
||||
|
||||
|
||||
##############################################################################
|
||||
# The training loop is exactly the same as in the GCN tutorial.
|
||||
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
g, features, labels, mask = load_cora_data()
|
||||
|
||||
# create the model, 2 heads, each head has hidden size 8
|
||||
net = GAT(g, in_dim=features.size()[1], hidden_dim=8, out_dim=7, num_heads=2)
|
||||
|
||||
# create optimizer
|
||||
optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)
|
||||
|
||||
# main loop
|
||||
dur = []
|
||||
for epoch in range(30):
|
||||
if epoch >= 3:
|
||||
t0 = time.time()
|
||||
|
||||
logits = net(features)
|
||||
logp = F.log_softmax(logits, 1)
|
||||
loss = F.nll_loss(logp[mask], labels[mask])
|
||||
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
if epoch >= 3:
|
||||
dur.append(time.time() - t0)
|
||||
|
||||
print(
|
||||
"Epoch {:05d} | Loss {:.4f} | Time(s) {:.4f}".format(
|
||||
epoch, loss.item(), np.mean(dur)
|
||||
)
|
||||
)
|
||||
|
||||
#########################################################################
|
||||
# Visualizing and understanding attention learned
|
||||
# ----------------------------------------------
|
||||
#
|
||||
# Cora
|
||||
# ^^^^
|
||||
#
|
||||
# The following table summarizes the model performance on Cora that is reported in
|
||||
# `the GAT paper <https://arxiv.org/pdf/1710.10903.pdf>`_ and obtained with DGL
|
||||
# implementations.
|
||||
#
|
||||
# .. list-table::
|
||||
# :header-rows: 1
|
||||
#
|
||||
# * - Model
|
||||
# - Accuracy
|
||||
# * - GCN (paper)
|
||||
# - :math:`81.4\pm 0.5%`
|
||||
# * - GCN (dgl)
|
||||
# - :math:`82.05\pm 0.33%`
|
||||
# * - GAT (paper)
|
||||
# - :math:`83.0\pm 0.7%`
|
||||
# * - GAT (dgl)
|
||||
# - :math:`83.69\pm 0.529%`
|
||||
#
|
||||
# *What kind of attention distribution has our model learned?*
|
||||
#
|
||||
# Because the attention weight :math:`a_{ij}` is associated with edges, you can
|
||||
# visualize it by coloring edges. Below you can pick a subgraph of Cora and plot the
|
||||
# attention weights of the last ``GATLayer``. The nodes are colored according
|
||||
# to their labels, whereas the edges are colored according to the magnitude of
|
||||
# the attention weights, which can be referred with the colorbar on the right.
|
||||
#
|
||||
# .. image:: https://data.dgl.ai/tutorial/gat/cora-attention.png
|
||||
# :width: 600px
|
||||
# :align: center
|
||||
#
|
||||
# You can see that the model seems to learn different attention weights. To
|
||||
# understand the distribution more thoroughly, measure the `entropy
|
||||
# <https://en.wikipedia.org/wiki/Entropy_(information_theory>`_) of the
|
||||
# attention distribution. For any node :math:`i`,
|
||||
# :math:`\{\alpha_{ij}\}_{j\in\mathcal{N}(i)}` forms a discrete probability
|
||||
# distribution over all its neighbors with the entropy given by
|
||||
#
|
||||
# .. math:: H({\alpha_{ij}}_{j\in\mathcal{N}(i)})=-\sum_{j\in\mathcal{N}(i)} \alpha_{ij}\log\alpha_{ij}
|
||||
#
|
||||
# A low entropy means a high degree of concentration, and vice
|
||||
# versa. An entropy of 0 means all attention is on one source node. The uniform
|
||||
# distribution has the highest entropy of :math:`\log(\mathcal{N}(i))`.
|
||||
# Ideally, you want to see the model learns a distribution of lower entropy
|
||||
# (i.e, one or two neighbors are much more important than the others).
|
||||
#
|
||||
# Note that since nodes can have different degrees, the maximum entropy will
|
||||
# also be different. Therefore, you plot the aggregated histogram of entropy
|
||||
# values of all nodes in the entire graph. Below are the attention histogram of
|
||||
# learned by each attention head.
|
||||
#
|
||||
# |image2|
|
||||
#
|
||||
# As a reference, here is the histogram if all the nodes have uniform attention weight distribution.
|
||||
#
|
||||
# .. image:: https://data.dgl.ai/tutorial/gat/cora-attention-uniform-hist.png
|
||||
# :width: 250px
|
||||
# :align: center
|
||||
#
|
||||
# One can see that **the attention values learned is quite similar to uniform distribution**
|
||||
# (i.e, all neighbors are equally important). This partially
|
||||
# explains why the performance of GAT is close to that of GCN on Cora
|
||||
# (according to `author's reported result
|
||||
# <https://arxiv.org/pdf/1710.10903.pdf>`_, the accuracy difference averaged
|
||||
# over 100 runs is less than 2 percent). Attention does not matter
|
||||
# since it does not differentiate much.
|
||||
#
|
||||
# *Does that mean the attention mechanism is not useful?* No! A different
|
||||
# dataset exhibits an entirely different pattern, as you can see next.
|
||||
#
|
||||
# Protein-protein interaction (PPI) networks
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
#
|
||||
# The PPI dataset used here consists of :math:`24` graphs corresponding to
|
||||
# different human tissues. Nodes can have up to :math:`121` kinds of labels, so
|
||||
# the label of node is represented as a binary tensor of size :math:`121`. The
|
||||
# task is to predict node label.
|
||||
#
|
||||
# Use :math:`20` graphs for training, :math:`2` for validation and :math:`2`
|
||||
# for test. The average number of nodes per graph is :math:`2372`. Each node
|
||||
# has :math:`50` features that are composed of positional gene sets, motif gene
|
||||
# sets, and immunological signatures. Critically, test graphs remain completely
|
||||
# unobserved during training, a setting called "inductive learning".
|
||||
#
|
||||
# Compare the performance of GAT and GCN for :math:`10` random runs on this
|
||||
# task and use hyperparameter search on the validation set to find the best
|
||||
# model.
|
||||
#
|
||||
# .. list-table::
|
||||
# :header-rows: 1
|
||||
#
|
||||
# * - Model
|
||||
# - F1 Score(micro)
|
||||
# * - GAT
|
||||
# - :math:`0.975 \pm 0.006`
|
||||
# * - GCN
|
||||
# - :math:`0.509 \pm 0.025`
|
||||
# * - Paper
|
||||
# - :math:`0.973 \pm 0.002`
|
||||
#
|
||||
# The table above is the result of this experiment, where you use micro `F1
|
||||
# score <https://en.wikipedia.org/wiki/F1_score>`_ to evaluate the model
|
||||
# performance.
|
||||
#
|
||||
# .. note::
|
||||
#
|
||||
# Below is the calculation process of F1 score:
|
||||
#
|
||||
# .. math::
|
||||
#
|
||||
# precision=\frac{\sum_{t=1}^{n}TP_{t}}{\sum_{t=1}^{n}(TP_{t} +FP_{t})}
|
||||
#
|
||||
# recall=\frac{\sum_{t=1}^{n}TP_{t}}{\sum_{t=1}^{n}(TP_{t} +FN_{t})}
|
||||
#
|
||||
# F1_{micro}=2\frac{precision*recall}{precision+recall}
|
||||
#
|
||||
# * :math:`TP_{t}` represents for number of nodes that both have and are predicted to have label :math:`t`
|
||||
# * :math:`FP_{t}` represents for number of nodes that do not have but are predicted to have label :math:`t`
|
||||
# * :math:`FN_{t}` represents for number of output classes labeled as :math:`t` but predicted as others.
|
||||
# * :math:`n` is the number of labels, i.e. :math:`121` in our case.
|
||||
#
|
||||
# During training, use ``BCEWithLogitsLoss`` as the loss function. The
|
||||
# learning curves of GAT and GCN are presented below; what is evident is the
|
||||
# dramatic performance adavantage of GAT over GCN.
|
||||
#
|
||||
# .. image:: https://data.dgl.ai/tutorial/gat/ppi-curve.png
|
||||
# :width: 300px
|
||||
# :align: center
|
||||
#
|
||||
# As before, you can have a statistical understanding of the attentions learned
|
||||
# by showing the histogram plot for the node-wise attention entropy. Below are
|
||||
# the attention histograms learned by different attention layers.
|
||||
#
|
||||
# *Attention learned in layer 1:*
|
||||
#
|
||||
# |image5|
|
||||
#
|
||||
# *Attention learned in layer 2:*
|
||||
#
|
||||
# |image6|
|
||||
#
|
||||
# *Attention learned in final layer:*
|
||||
#
|
||||
# |image7|
|
||||
#
|
||||
# Again, comparing with uniform distribution:
|
||||
#
|
||||
# .. image:: https://data.dgl.ai/tutorial/gat/ppi-uniform-hist.png
|
||||
# :width: 250px
|
||||
# :align: center
|
||||
#
|
||||
# Clearly, **GAT does learn sharp attention weights**! There is a clear pattern
|
||||
# over the layers as well: **the attention gets sharper with a higher
|
||||
# layer**.
|
||||
#
|
||||
# Unlike the Cora dataset where GAT's gain is minimal at best, for PPI there
|
||||
# is a significant performance gap between GAT and other GNN variants compared
|
||||
# in `the GAT paper <https://arxiv.org/pdf/1710.10903.pdf>`_ (at least 20 percent),
|
||||
# and the attention distributions between the two clearly differ. While this
|
||||
# deserves further research, one immediate conclusion is that GAT's advantage
|
||||
# lies perhaps more in its ability to handle a graph with more complex
|
||||
# neighborhood structure.
|
||||
#
|
||||
# What's next?
|
||||
# ------------
|
||||
#
|
||||
# So far, you have seen how to use DGL to implement GAT. There are some
|
||||
# missing details such as dropout, skip connections, and hyper-parameter tuning,
|
||||
# which are practices that do not involve DGL-related concepts. For more information
|
||||
# check out the full example.
|
||||
#
|
||||
# * See the optimized `full example <https://github.com/dmlc/dgl/blob/master/examples/pytorch/gat/gat.py>`_.
|
||||
# * The next tutorial describes how to speedup GAT models by parallelizing multiple attention heads and SPMV optimization.
|
||||
#
|
||||
# .. |image2| image:: https://data.dgl.ai/tutorial/gat/cora-attention-hist.png
|
||||
# .. |image5| image:: https://data.dgl.ai/tutorial/gat/ppi-first-layer-hist.png
|
||||
# .. |image6| image:: https://data.dgl.ai/tutorial/gat/ppi-second-layer-hist.png
|
||||
# .. |image7| image:: https://data.dgl.ai/tutorial/gat/ppi-final-layer-hist.png
|
||||
@@ -0,0 +1,37 @@
|
||||
.. _tutorials1-index:
|
||||
|
||||
Graph neural networks and its variants
|
||||
--------------------------------------------
|
||||
|
||||
* **Graph convolutional network (GCN)** `[research paper] <https://arxiv.org/abs/1609.02907>`__ `[tutorial]
|
||||
<1_gnn/1_gcn.html>`__ `[Pytorch code]
|
||||
<https://github.com/dmlc/dgl/blob/master/examples/pytorch/gcn>`__
|
||||
`[MXNet code]
|
||||
<https://github.com/dmlc/dgl/tree/master/examples/mxnet/gcn>`__:
|
||||
|
||||
* **Graph attention network (GAT)** `[research paper] <https://arxiv.org/abs/1710.10903>`__ `[tutorial]
|
||||
<1_gnn/9_gat.html>`__ `[Pytorch code]
|
||||
<https://github.com/dmlc/dgl/blob/master/examples/pytorch/gat>`__
|
||||
`[MXNet code]
|
||||
<https://github.com/dmlc/dgl/tree/master/examples/mxnet/gat>`__:
|
||||
GAT extends the GCN functionality by deploying multi-head attention
|
||||
among neighborhood of a node. This greatly enhances the capacity and
|
||||
expressiveness of the model.
|
||||
|
||||
* **Relational-GCN** `[research paper] <https://arxiv.org/abs/1703.06103>`__ `[tutorial]
|
||||
<1_gnn/4_rgcn.html>`__ `[Pytorch code]
|
||||
<https://github.com/dmlc/dgl/tree/master/examples/pytorch/rgcn>`__
|
||||
`[MXNet code]
|
||||
<https://github.com/dmlc/dgl/tree/master/examples/mxnet/rgcn>`__:
|
||||
Relational-GCN allows multiple edges among two entities of a
|
||||
graph. Edges with distinct relationships are encoded differently.
|
||||
|
||||
* **Line graph neural network (LGNN)** `[research paper] <https://openreview.net/pdf?id=H1g0Z3A9Fm>`__ `[tutorial]
|
||||
<1_gnn/6_line_graph.html>`__ `[Pytorch code]
|
||||
<https://github.com/dmlc/dgl/tree/master/examples/pytorch/line_graph>`__:
|
||||
This network focuses on community detection by inspecting graph structures. It
|
||||
uses representations of both the original graph and its line-graph
|
||||
companion. In addition to demonstrating how an algorithm can harness multiple
|
||||
graphs, this implementation shows how you can judiciously mix simple tensor
|
||||
operations and sparse-matrix tensor operations, along with message-passing with
|
||||
DGL.
|
||||
Reference in New Issue
Block a user