Skip to content

Commit

Permalink
Merge branch 'master' into assortativity
Browse files Browse the repository at this point in the history
  • Loading branch information
EdisonLeeeee authored Oct 5, 2022
2 parents 11eba35 + 6ca2332 commit 9e56273
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 5 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
## [2.2.0] - 2022-MM-DD
### Added
- Added `assortativity` that computes degree assortativity coefficient ([#5587](https://github.com/pyg-team/pytorch_geometric/pull/5587))
- Added `SSGConv` layer ([#5599](https://github.com/pyg-team/pytorch_geometric/pull/5599))
- Added `shuffle_node`, `mask_feature` and `add_random_edge` augmentation methdos ([#5548](https://github.com/pyg-team/pytorch_geometric/pull/5548))
- Added `dropout_path` augmentation that drops edges from a graph based on random walks ([#5531](https://github.com/pyg-team/pytorch_geometric/pull/5531))
- Add support for filling labels with dummy values in `HeteroData.to_homogeneous()` ([#5540](https://github.com/pyg-team/pytorch_geometric/pull/5540))
Expand Down Expand Up @@ -36,6 +37,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Added `BaseStorage.get()` functionality ([#5240](https://github.com/pyg-team/pytorch_geometric/pull/5240))
- Added a test to confirm that `to_hetero` works with `SparseTensor` ([#5222](https://github.com/pyg-team/pytorch_geometric/pull/5222))
### Changed
- Fix `RGCN+pyg-lib` for `LongTensor` input ([#5610](https://github.com/pyg-team/pytorch_geometric/pull/5610))
- Improved type hint support ([#5603](https://github.com/pyg-team/pytorch_geometric/pull/5603))
- Avoid modifying `mode_kwargs` in `MultiAggregation` ([#5601](https://github.com/pyg-team/pytorch_geometric/pull/5601))
- Changed `BatchNorm` to allow for batches of size one during training ([#5530](https://github.com/pyg-team/pytorch_geometric/pull/5530))
- Integrated better temporal sampling support by requiring that local neighborhoods are sorted according to time ([#5516](https://github.com/pyg-team/pytorch_geometric/issues/5516), [#5602](https://github.com/pyg-team/pytorch_geometric/issues/5602))
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ These GNN layers can be stacked together to create Graph Neural Network models.
* **[HGTConv](https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html#torch_geometric.nn.conv.HGTConv)** from Hu *et al.*: [Heterogeneous Graph Transformer](https://arxiv.org/abs/2003.01332) (WWW 2020) [[**Example**](https://github.com/pyg-team/pytorch_geometric/blob/master/examples/hetero/hgt_dblp.py)]
* **[HEATConv](https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html#torch_geometric.nn.conv.HEATonv)** from Mo *et al.*: [Heterogeneous Edge-Enhanced Graph Attention Network For Multi-Agent Trajectory Prediction](https://arxiv.org/abs/2106.07161) (CoRR 2021)
* A **[MetaLayer](https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html#torch_geometric.nn.meta.MetaLayer)** for building any kind of graph network similar to the [TensorFlow Graph Nets library](https://github.com/deepmind/graph_nets) from Battaglia *et al.*: [Relational Inductive Biases, Deep Learning, and Graph Networks](https://arxiv.org/abs/1806.01261) (CoRR 2018)
* **[SSGConv](https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html#torch_geometric.nn.conv.SSGConv)** from Zhu *et al.*: [Simple Spectral Graph Convolution](https://openreview.net/forum?id=CYO5T-YjWZV) (ICLR 2021)
</details>

**Pooling layers:**
Expand Down
2 changes: 1 addition & 1 deletion examples/rgcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from torch_geometric.utils import k_hop_subgraph

parser = argparse.ArgumentParser()
parser.add_argument('--dataset', type=str,
parser.add_argument('--dataset', type=str, default='AIFB',
choices=['AIFB', 'MUTAG', 'BGS', 'AM'])
args = parser.parse_args()

Expand Down
39 changes: 39 additions & 0 deletions test/nn/conv/test_ssg_conv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import torch
from torch_sparse import SparseTensor

from torch_geometric.nn import SSGConv
from torch_geometric.testing import is_full_test


def test_ssg_conv():
x = torch.randn(4, 16)
edge_index = torch.tensor([[0, 0, 0, 1, 2, 3], [1, 2, 3, 0, 0, 0]])
row, col = edge_index
value = torch.rand(row.size(0))
adj2 = SparseTensor(row=row, col=col, value=value, sparse_sizes=(4, 4))
adj1 = adj2.set_value(None)

conv = SSGConv(16, 32, alpha=0.1, K=10)
assert conv.__repr__() == 'SSGConv(16, 32, K=10, alpha=0.1)'
out1 = conv(x, edge_index)
assert out1.size() == (4, 32)
assert torch.allclose(conv(x, adj1.t()), out1, atol=1e-6)
out2 = conv(x, edge_index, value)
assert out2.size() == (4, 32)
assert torch.allclose(conv(x, adj2.t()), out2, atol=1e-6)

if is_full_test():
t = '(Tensor, Tensor, OptTensor) -> Tensor'
jit = torch.jit.script(conv.jittable(t))
assert jit(x, edge_index).tolist() == out1.tolist()
assert jit(x, edge_index, value).tolist() == out2.tolist()

t = '(Tensor, SparseTensor, OptTensor) -> Tensor'
jit = torch.jit.script(conv.jittable(t))
assert torch.allclose(jit(x, adj1.t()), out1, atol=1e-6)
assert torch.allclose(jit(x, adj2.t()), out2, atol=1e-6)

conv.cached = True
conv(x, edge_index)
assert conv(x, edge_index).tolist() == out1.tolist()
assert torch.allclose(conv(x, adj1.t()), out1, atol=1e-6)
6 changes: 6 additions & 0 deletions test/utils/test_isolated.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import torch

from torch_geometric.testing import is_full_test
from torch_geometric.utils import (
contains_isolated_nodes,
remove_isolated_nodes,
Expand All @@ -11,6 +12,11 @@ def test_contains_isolated_nodes():
assert not contains_isolated_nodes(edge_index)
assert contains_isolated_nodes(edge_index, num_nodes=3)

if is_full_test():
jit = torch.jit.script(contains_isolated_nodes)
assert not jit(edge_index)
assert jit(edge_index, num_nodes=3)

edge_index = torch.tensor([[0, 1, 2, 0], [1, 0, 2, 0]])
assert contains_isolated_nodes(edge_index)

Expand Down
4 changes: 2 additions & 2 deletions torch_geometric/datasets/snap_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ def read_ego(files, name):
edge_index = torch.stack([row, col], dim=0)

edge_index, _ = coalesce(edge_index, None, N, N)
data = Data(x=x, edge_index=edge_index, circle=circle,
circle_batch=circle_batch)
data = EgoData(x=x, edge_index=edge_index, circle=circle,
circle_batch=circle_batch)

data_list.append(data)

Expand Down
2 changes: 2 additions & 0 deletions torch_geometric/nn/conv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from .hetero_conv import HeteroConv
from .han_conv import HANConv
from .lg_conv import LGConv
from .ssg_conv import SSGConv

__all__ = [
'MessagePassing',
Expand All @@ -69,6 +70,7 @@
'GINEConv',
'ARMAConv',
'SGConv',
'SSGConv',
'APPNP',
'MFConv',
'RGCNConv',
Expand Down
4 changes: 3 additions & 1 deletion torch_geometric/nn/conv/rgcn_conv.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,9 @@ def forward(self, x: Union[OptTensor, Tuple[OptTensor, Tensor]],
out = out + h.contiguous().view(-1, self.out_channels)

else: # No regularization/Basis-decomposition ========================
if self._WITH_PYG_LIB and isinstance(edge_index, Tensor):
if (self._WITH_PYG_LIB and self.num_bases is None
and x_l.is_floating_point()
and isinstance(edge_index, Tensor)):
if not self.is_sorted:
if (edge_type[1:] < edge_type[:-1]).any():
edge_type, perm = edge_type.sort()
Expand Down
122 changes: 122 additions & 0 deletions torch_geometric/nn/conv/ssg_conv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from typing import Optional

from torch import Tensor
from torch_sparse import SparseTensor, matmul

from torch_geometric.nn.conv import MessagePassing
from torch_geometric.nn.conv.gcn_conv import gcn_norm
from torch_geometric.nn.dense.linear import Linear
from torch_geometric.typing import Adj, OptTensor


class SSGConv(MessagePassing):
r"""The simple spectral graph convolutional operator from the
`"Simple Spectral Graph Convolution"
<https://openreview.net/forum?id=CYO5T-YjWZV>`_ paper
.. math::
\mathbf{X}^{\prime} = \frac{1}{K} \sum_{k=1}^K\left((1-\alpha)
{\left(\mathbf{\hat{D}}^{-1/2} \mathbf{\hat{A}}
\mathbf{\hat{D}}^{-1/2} \right)}^k
\mathbf{X}+\alpha \mathbf{X}\right) \mathbf{\Theta},
where :math:`\mathbf{\hat{A}} = \mathbf{A} + \mathbf{I}` denotes the
adjacency matrix with inserted self-loops and
:math:`\hat{D}_{ii} = \sum_{j=0} \hat{A}_{ij}` its diagonal degree matrix.
The adjacency matrix can include other values than :obj:`1` representing
edge weights via the optional :obj:`edge_weight` tensor.
:class:`~torch_geometric.nn.conv.SSGConv` is an improved operator of
:class:`~torch_geometric.nn.conv.SGConv` by introducing the :obj:`alpha`
parameter to address the oversmoothing issue.
Args:
in_channels (int): Size of each input sample, or :obj:`-1` to derive
the size from the first input(s) to the forward method.
out_channels (int): Size of each output sample.
alpha (float): Teleport probability :math:`\alpha \in [0, 1]`.
K (int, optional): Number of hops :math:`K`. (default: :obj:`1`)
cached (bool, optional): If set to :obj:`True`, the layer will cache
the computation of :math:`\frac{1}{K} \sum_{k=1}^K\left((1-\alpha)
{\left(\mathbf{\hat{D}}^{-1/2} \mathbf{\hat{A}}
\mathbf{\hat{D}}^{-1/2} \right)}^k \mathbf{X}+
\alpha \mathbf{X}\right)` on first execution, and will use the
cached version for further executions.
This parameter should only be set to :obj:`True` in transductive
learning scenarios. (default: :obj:`False`)
add_self_loops (bool, optional): If set to :obj:`False`, will not add
self-loops to the input graph. (default: :obj:`True`)
bias (bool, optional): If set to :obj:`False`, the layer will not learn
an additive bias. (default: :obj:`True`)
**kwargs (optional): Additional arguments of
:class:`torch_geometric.nn.conv.MessagePassing`.
Shapes:
- **input:**
node features :math:`(|\mathcal{V}|, F_{in})`,
edge indices :math:`(2, |\mathcal{E}|)`,
edge weights :math:`(|\mathcal{E}|)` *(optional)*
- **output:**
node features :math:`(|\mathcal{V}|, F_{out})`
"""

_cached_h: Optional[Tensor]

def __init__(self, in_channels: int, out_channels: int, alpha: float,
K: int = 1, cached: bool = False, add_self_loops: bool = True,
bias: bool = True, **kwargs):
kwargs.setdefault('aggr', 'add')
super().__init__(**kwargs)

self.in_channels = in_channels
self.out_channels = out_channels
self.alpha = alpha
self.K = K
self.cached = cached
self.add_self_loops = add_self_loops

self._cached_h = None

self.lin = Linear(in_channels, out_channels, bias=bias)

self.reset_parameters()

def reset_parameters(self):
self.lin.reset_parameters()
self._cached_h = None

def forward(self, x: Tensor, edge_index: Adj,
edge_weight: OptTensor = None) -> Tensor:
""""""
cache = self._cached_h
if cache is None:
if isinstance(edge_index, Tensor):
edge_index, edge_weight = gcn_norm( # yapf: disable
edge_index, edge_weight, x.size(self.node_dim), False,
self.add_self_loops, self.flow, dtype=x.dtype)
elif isinstance(edge_index, SparseTensor):
edge_index = gcn_norm( # yapf: disable
edge_index, edge_weight, x.size(self.node_dim), False,
self.add_self_loops, self.flow, dtype=x.dtype)

h = x * self.alpha
for k in range(self.K):
# propagate_type: (x: Tensor, edge_weight: OptTensor)
x = self.propagate(edge_index, x=x, edge_weight=edge_weight,
size=None)
h = h + (1 - self.alpha) / self.K * x
if self.cached:
self._cached_h = h
else:
h = cache.detach()

return self.lin(h)

def message(self, x_j: Tensor, edge_weight: Tensor) -> Tensor:
return edge_weight.view(-1, 1) * x_j

def message_and_aggregate(self, adj_t: SparseTensor, x: Tensor) -> Tensor:
return matmul(adj_t, x, reduce=self.aggr)

def __repr__(self) -> str:
return (f'{self.__class__.__name__}({self.in_channels}, '
f'{self.out_channels}, K={self.K}, alpha={self.alpha})')
8 changes: 7 additions & 1 deletion torch_geometric/utils/isolated.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from typing import Optional

import torch
from torch import Tensor

from torch_geometric.utils import remove_self_loops, segregate_self_loops

from .num_nodes import maybe_num_nodes


def contains_isolated_nodes(edge_index, num_nodes=None):
def contains_isolated_nodes(
edge_index: Tensor,
num_nodes: Optional[int] = None,
) -> bool:
r"""Returns :obj:`True` if the graph given by :attr:`edge_index` contains
isolated nodes.
Expand Down

0 comments on commit 9e56273

Please sign in to comment.