Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Reversed Graph and Transform Module #331

Merged
merged 32 commits into from
Jan 4, 2019
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/api/python/graph.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Transforming graph
DGLGraph.subgraphs
DGLGraph.edge_subgraph
DGLGraph.line_graph
DGLGraph.reverse

Converting from/to other format
-------------------------------
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/python/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ API Reference
udf
sampler
data
transform
12 changes: 12 additions & 0 deletions docs/source/api/python/transform.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.. _apigraph:

Transform -- Graph Transformation
=================================

.. automodule:: dgl.transform

.. autosummary::
:toctree: ../../generated/

line_graph
reverse
2 changes: 1 addition & 1 deletion docs/source/install/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The backend is controlled by ``DGLBACKEND`` environment variable, which defaults
| | | `official website <https://pytorch.org>`_ |
+---------+---------+--------------------------------------------------+
| mxnet | MXNet | Requires nightly build; run the following |
| | | command to install (TODO): |
| | | command to install: |
| | | |
| | | .. code:: bash |
| | | |
Expand Down
1 change: 1 addition & 0 deletions python/dgl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
from .traversal import *
from .propagate import *
from .udf import NodeBatch, EdgeBatch
from .transform import *
21 changes: 8 additions & 13 deletions python/dgl/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from collections import defaultdict

import dgl
from .base import ALL, is_all, DGLError
from . import backend as F
from . import init
Expand Down Expand Up @@ -2760,22 +2761,16 @@ def incidence_matrix(self, typestr, ctx=F.cpu()):
def line_graph(self, backtracking=True, shared=False):
"""Return the line graph of this graph.

Parameters
----------
backtracking : bool, optional
Whether the returned line graph is backtracking.
See :func:`~dgl.transform.line_graph`.
"""
return dgl.line_graph(self, backtracking, shared)

shared : bool, optional
Whether the returned line graph shares representations with `self`.
def reverse(self, share_node_attrs=True, share_edge_attrs=True):
jermainewang marked this conversation as resolved.
Show resolved Hide resolved
"""Return the reverse of this graph.

Returns
-------
DGLGraph
The line graph of this graph.
See :func:`~dgl.transform.reverse`.
"""
graph_data = self._graph.line_graph(backtracking)
node_frame = self._edge_frame if shared else None
return DGLGraph(graph_data, node_frame)
return dgl.reverse(self, share_node_attrs, share_edge_attrs)

def filter_nodes(self, predicate, nodes=ALL):
"""Return a tensor of node IDs that satisfy the given predicate.
Expand Down
118 changes: 118 additions & 0 deletions python/dgl/transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Module for graph transformation methods."""
from copy import deepcopy

from .graph import DGLGraph
from .batched_graph import BatchedDGLGraph

__all__ = ['line_graph', 'reverse']


def line_graph(g, backtracking=True, shared=False):
"""Return the line graph of this graph.

Parameters
----------
g : dgl.DGLGraph
backtracking : bool, optional
Whether the returned line graph is backtracking.
shared : bool, optional
Whether the returned line graph shares representations with `self`.

Returns
-------
DGLGraph
The line graph of this graph.
"""
graph_data = g._graph.line_graph(backtracking)
node_frame = g._edge_frame if shared else None
return DGLGraph(graph_data, node_frame)

def reverse(g, share_node_attrs=True, share_edge_attrs=True):
jermainewang marked this conversation as resolved.
Show resolved Hide resolved
"""Return the reverse of a graph

The reverse (also called converse, transpose) of a directed graph is another directed
graph on the same nodes with edges reversed in terms of direction.

Given a :class:`DGLGraph` object, we return another :class:`DGLGraph` object
representing its reverse.

If the original graph has node features, its reverse will have the same features.
If the original graph has edge features, a reversed edge will have the same feature
as the original one. We allow the option for the two graphs to share memory for their
node/edge features.

If the original graph has registered `apply_edge_func`, `apply_node_func`, `message_func`
or `reduce_func`, the reverse will have the same.

Notes
-----
* This function does not support :class:`~dgl.BatchedDGLGraph` objects.
* We do not dynamically update the topology of a graph once that of its reverse changes.
This can be particularly problematic when the node/edge attrs are shared. For example,
if the topology of both the original graph and its reverse get changed independently,
you can get a mismatched node/edge feature.

Examples
--------
Create a graph to reverse.

>>> import dgl
>>> import torch as th
>>> g = dgl.DGLGraph()
>>> g.add_nodes(3)
>>> g.add_edges([0, 1, 2], [1, 2, 0])
>>> g.ndata['h'] = th.tensor([[0.], [1.], [2.]])
>>> g.edata['h'] = th.tensor([[3.], [4.], [5.]])

Reverse the graph and examine its structure.

>>> rg = dgl.reverse(g)
>>> print(rg)
DGLGraph with 3 nodes and 3 edges.
Node data: {'h': Scheme(shape=(1,), dtype=torch.float32)}
Edge data: {'h': Scheme(shape=(1,), dtype=torch.float32)}

The edges are reversed now.

>>> rg.has_edges_between([1, 2, 0], [0, 1, 2])
tensor([1, 1, 1])

Reversed edges have the same feature as the original ones.

>>> g.edges[[0, 2], [1, 0]].data['h'] == rg.edges[[1, 0], [0, 2]].data['h']
tensor([[1],
[1]], dtype=torch.uint8)

The node/edge features of the reversed graph share memory with the original
graph, which is helpful for both forward computation and back propagation.

>>> g.ndata['h'] = g.ndata['h'] + 1
>>> rg.ndata['h']
tensor([[1.],
[2.],
[3.]])

Parameters
----------
g : dgl.DGLGraph
share_node_attrs: bool, optional
If True, the original graph and the reversed graph share memory for node
attributes.
share_edge_attrs: bool, optional
If True, the original graph and the reversed graph share memory for edge
attributes.
"""
assert not isinstance(g, BatchedDGLGraph), \
'reverse is not supported for a BatchedDGLGraph object'
g_reversed = DGLGraph(multigraph=g.is_multigraph)
g_reversed.add_nodes(g.number_of_nodes())
g_reversed.add_edges(g.edges()[1], g.edges()[0])
jermainewang marked this conversation as resolved.
Show resolved Hide resolved
if share_node_attrs:
g_reversed._node_frame = g._node_frame
else:
g_reversed._node_frame = deepcopy(g._node_frame)
if share_edge_attrs:
g_reversed._edge_frame = g._edge_frame
else:
g_reversed._edge_frame = deepcopy(g._edge_frame)
jermainewang marked this conversation as resolved.
Show resolved Hide resolved
return g_reversed
44 changes: 0 additions & 44 deletions tests/pytorch/test_line_graph.py

This file was deleted.

106 changes: 106 additions & 0 deletions tests/pytorch/test_transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import torch as th
import networkx as nx
import numpy as np
import dgl
import dgl.function as fn
import utils as U

D = 5

# line graph related
def test_line_graph():
N = 5
G = dgl.DGLGraph(nx.star_graph(N))
G.edata['h'] = th.randn((2 * N, D))
n_edges = G.number_of_edges()
L = G.line_graph(shared=True)
assert L.number_of_nodes() == 2 * N
L.ndata['h'] = th.randn((2 * N, D))
# update node features on line graph should reflect to edge features on
# original graph.
u = [0, 0, 2, 3]
v = [1, 2, 0, 0]
eid = G.edge_ids(u, v)
L.nodes[eid].data['h'] = th.zeros((4, D))
assert U.allclose(G.edges[u, v].data['h'], th.zeros((4, D)))

# adding a new node feature on line graph should also reflect to a new
# edge feature on original graph
data = th.randn(n_edges, D)
L.ndata['w'] = data
assert U.allclose(G.edata['w'], data)

def test_no_backtracking():
N = 5
G = dgl.DGLGraph(nx.star_graph(N))
L = G.line_graph(backtracking=False)
assert L.number_of_nodes() == 2 * N
for i in range(1, N):
e1 = G.edge_id(0, i)
e2 = G.edge_id(i, 0)
assert not L.has_edge_between(e1, e2)
assert not L.has_edge_between(e2, e1)

# reverse graph related
def test_reverse_no_share():
g = dgl.DGLGraph()
g.add_nodes(5)
# The graph need not to be completely connected.
g.add_edges([0, 1, 2], [1, 2, 1])
g.ndata['h'] = th.tensor([[0.], [1.], [2.], [3.], [4.]])
g.edata['h'] = th.tensor([[5.], [6.], [7.]])
rg = g.reverse(share_node_attrs=False, share_edge_attrs=False)

assert g.is_multigraph == rg.is_multigraph

assert g.number_of_nodes() == rg.number_of_nodes()
assert g.number_of_edges() == rg.number_of_edges()
assert U.allclose(rg.has_edges_between([1, 2, 1], [0, 1, 2]).float(), th.ones(3))
assert g.edge_id(0, 1) == rg.edge_id(1, 0)
assert g.edge_id(1, 2) == rg.edge_id(2, 1)
assert g.edge_id(2, 1) == rg.edge_id(1, 2)

g.ndata['h'] = g.ndata['h'] + 1
assert not U.allclose(g.ndata['h'], rg.ndata['h'])
rg.edata['h'] = rg.edata['h'] + 1
assert not U.allclose(g.edata['h'], rg.edata['h'])

def test_reverse_shared_frames():
g = dgl.DGLGraph()
g.add_nodes(3)
g.add_edges([0, 1, 2], [1, 2, 1])
g.ndata['h'] = th.tensor([[0.], [1.], [2.]], requires_grad=True)
g.edata['h'] = th.tensor([[3.], [4.], [5.]], requires_grad=True)

rg = g.reverse()
assert U.allclose(g.ndata['h'], rg.ndata['h'])
assert U.allclose(g.edata['h'], rg.edata['h'])
assert U.allclose(g.edges[[0, 2], [1, 1]].data['h'],
rg.edges[[1, 1], [0, 2]].data['h'])

rg.ndata['h'] = rg.ndata['h'] + 1
assert U.allclose(rg.ndata['h'], g.ndata['h'])

g.edata['h'] = g.edata['h'] - 1
assert U.allclose(rg.edata['h'], g.edata['h'])

src_msg = fn.copy_src(src='h', out='m')
sum_reduce = fn.sum(msg='m', out='h')

rg.update_all(src_msg, sum_reduce)
assert U.allclose(g.ndata['h'], rg.ndata['h'])

# Grad check
g.ndata['h'].retain_grad()
rg.ndata['h'].retain_grad()
loss_func = th.nn.MSELoss()
target = th.zeros(3, 1)
loss = loss_func(rg.ndata['h'], target)
loss.backward()
assert U.allclose(g.ndata['h'].grad, rg.ndata['h'].grad)

if __name__ == '__main__':
test_line_graph()
test_no_backtracking()
test_reverse_no_share()
test_reverse_shared_frames()