Unverified Commit b576e617 authored by Quan (Andy) Gan's avatar Quan (Andy) Gan Committed by GitHub
Browse files

[Feature] Add left normalizer for GCN (#3114)

* add left normalizer for gcn

* fix

* fixes and some bug stuff
parent fac75e16
...@@ -12,6 +12,8 @@ Requirements ...@@ -12,6 +12,8 @@ Requirements
pip install -r requirements.txt pip install -r requirements.txt
``` ```
Also requires PyTorch 1.7.0+.
Datasets Datasets
-------- --------
......
...@@ -14,7 +14,10 @@ from ... import backend as F ...@@ -14,7 +14,10 @@ from ... import backend as F
from ...base import DGLError from ...base import DGLError
from ...utils import to_dgl_context from ...utils import to_dgl_context
__all__ = ['NodeDataLoader', 'EdgeDataLoader', 'GraphDataLoader'] __all__ = ['NodeDataLoader', 'EdgeDataLoader', 'GraphDataLoader',
# Temporary exposure.
'_pop_subgraph_storage', '_pop_blocks_storage',
'_restore_subgraph_storage', '_restore_blocks_storage']
PYTORCH_VER = LooseVersion(th.__version__) PYTORCH_VER = LooseVersion(th.__version__)
PYTORCH_16 = PYTORCH_VER >= LooseVersion("1.6.0") PYTORCH_16 = PYTORCH_VER >= LooseVersion("1.6.0")
......
...@@ -32,10 +32,18 @@ class GraphConv(gluon.Block): ...@@ -32,10 +32,18 @@ class GraphConv(gluon.Block):
out_feats : int out_feats : int
Output feature size; i.e., the number of dimensions of :math:`h_i^{(l+1)}`. Output feature size; i.e., the number of dimensions of :math:`h_i^{(l+1)}`.
norm : str, optional norm : str, optional
How to apply the normalizer. If is `'right'`, divide the aggregated messages How to apply the normalizer. Can be one of the following values:
by each node's in-degrees, which is equivalent to averaging the received messages.
If is `'none'`, no normalization is applied. Default is `'both'`, * ``right``, to divide the aggregated messages by each node's in-degrees,
where the :math:`c_{ij}` in the paper is applied. which is equivalent to averaging the received messages.
* ``none``, where no normalization is applied.
* ``both`` (default), where the messages are scaled with :math:`1/c_{ji}` above, equivalent
to symmetric normalization.
* ``left``, to divide the messages sent out from each node by its out-degrees,
equivalent to random walk normalization.
weight : bool, optional weight : bool, optional
If True, apply a linear layer. Otherwise, aggregating the messages If True, apply a linear layer. Otherwise, aggregating the messages
without a weight matrix. without a weight matrix.
...@@ -136,8 +144,8 @@ class GraphConv(gluon.Block): ...@@ -136,8 +144,8 @@ class GraphConv(gluon.Block):
activation=None, activation=None,
allow_zero_in_degree=False): allow_zero_in_degree=False):
super(GraphConv, self).__init__() super(GraphConv, self).__init__()
if norm not in ('none', 'both', 'right'): if norm not in ('none', 'both', 'right', 'left'):
raise DGLError('Invalid norm value. Must be either "none", "both" or "right".' raise DGLError('Invalid norm value. Must be either "none", "both", "right" or "left".'
' But got "{}".'.format(norm)) ' But got "{}".'.format(norm))
self._in_feats = in_feats self._in_feats = in_feats
self._out_feats = out_feats self._out_feats = out_feats
...@@ -230,15 +238,18 @@ class GraphConv(gluon.Block): ...@@ -230,15 +238,18 @@ class GraphConv(gluon.Block):
'suppress the check and let the code run.') 'suppress the check and let the code run.')
feat_src, feat_dst = expand_as_pair(feat, graph) feat_src, feat_dst = expand_as_pair(feat, graph)
if self._norm in ['both', 'left']:
if self._norm == 'both': degs = graph.out_degrees().as_in_context(feat_dst.context).astype('float32')
degs = graph.out_degrees().as_in_context(feat_src.context).astype('float32')
degs = mx.nd.clip(degs, a_min=1, a_max=float("inf")) degs = mx.nd.clip(degs, a_min=1, a_max=float("inf"))
norm = mx.nd.power(degs, -0.5) if self._norm == 'both':
norm = mx.nd.power(degs, -0.5)
else:
norm = 1.0 / degs
shp = norm.shape + (1,) * (feat_src.ndim - 1) shp = norm.shape + (1,) * (feat_src.ndim - 1)
norm = norm.reshape(shp) norm = norm.reshape(shp)
feat_src = feat_src * norm feat_src = feat_src * norm
if weight is not None: if weight is not None:
if self.weight is not None: if self.weight is not None:
raise DGLError('External weight is provided while at the same time the' raise DGLError('External weight is provided while at the same time the'
...@@ -264,7 +275,7 @@ class GraphConv(gluon.Block): ...@@ -264,7 +275,7 @@ class GraphConv(gluon.Block):
if weight is not None: if weight is not None:
rst = mx.nd.dot(rst, weight) rst = mx.nd.dot(rst, weight)
if self._norm != 'none': if self._norm in ['both', 'right']:
degs = graph.in_degrees().as_in_context(feat_dst.context).astype('float32') degs = graph.in_degrees().as_in_context(feat_dst.context).astype('float32')
degs = mx.nd.clip(degs, a_min=1, a_max=float("inf")) degs = mx.nd.clip(degs, a_min=1, a_max=float("inf"))
if self._norm == 'both': if self._norm == 'both':
......
...@@ -173,10 +173,18 @@ class GraphConv(nn.Module): ...@@ -173,10 +173,18 @@ class GraphConv(nn.Module):
out_feats : int out_feats : int
Output feature size; i.e., the number of dimensions of :math:`h_i^{(l+1)}`. Output feature size; i.e., the number of dimensions of :math:`h_i^{(l+1)}`.
norm : str, optional norm : str, optional
How to apply the normalizer. If is `'right'`, divide the aggregated messages How to apply the normalizer. Can be one of the following values:
by each node's in-degrees, which is equivalent to averaging the received messages.
If is `'none'`, no normalization is applied. Default is `'both'`, * ``right``, to divide the aggregated messages by each node's in-degrees,
where the :math:`c_{ji}` in the paper is applied. which is equivalent to averaging the received messages.
* ``none``, where no normalization is applied.
* ``both`` (default), where the messages are scaled with :math:`1/c_{ji}` above, equivalent
to symmetric normalization.
* ``left``, to divide the messages sent out from each node by its out-degrees,
equivalent to random walk normalization.
weight : bool, optional weight : bool, optional
If True, apply a linear layer. Otherwise, aggregating the messages If True, apply a linear layer. Otherwise, aggregating the messages
without a weight matrix. without a weight matrix.
...@@ -270,8 +278,8 @@ class GraphConv(nn.Module): ...@@ -270,8 +278,8 @@ class GraphConv(nn.Module):
activation=None, activation=None,
allow_zero_in_degree=False): allow_zero_in_degree=False):
super(GraphConv, self).__init__() super(GraphConv, self).__init__()
if norm not in ('none', 'both', 'right'): if norm not in ('none', 'both', 'right', 'left'):
raise DGLError('Invalid norm value. Must be either "none", "both" or "right".' raise DGLError('Invalid norm value. Must be either "none", "both", "right" or "left".'
' But got "{}".'.format(norm)) ' But got "{}".'.format(norm))
self._in_feats = in_feats self._in_feats = in_feats
self._out_feats = out_feats self._out_feats = out_feats
...@@ -395,9 +403,12 @@ class GraphConv(nn.Module): ...@@ -395,9 +403,12 @@ class GraphConv(nn.Module):
# (BarclayII) For RGCN on heterogeneous graphs we need to support GCN on bipartite. # (BarclayII) For RGCN on heterogeneous graphs we need to support GCN on bipartite.
feat_src, feat_dst = expand_as_pair(feat, graph) feat_src, feat_dst = expand_as_pair(feat, graph)
if self._norm == 'both': if self._norm in ['left', 'both']:
degs = graph.out_degrees().float().clamp(min=1) degs = graph.out_degrees().float().clamp(min=1)
norm = th.pow(degs, -0.5) if self._norm == 'both':
norm = th.pow(degs, -0.5)
else:
norm = 1.0 / degs
shp = norm.shape + (1,) * (feat_src.dim() - 1) shp = norm.shape + (1,) * (feat_src.dim() - 1)
norm = th.reshape(norm, shp) norm = th.reshape(norm, shp)
feat_src = feat_src * norm feat_src = feat_src * norm
...@@ -425,7 +436,7 @@ class GraphConv(nn.Module): ...@@ -425,7 +436,7 @@ class GraphConv(nn.Module):
if weight is not None: if weight is not None:
rst = th.matmul(rst, weight) rst = th.matmul(rst, weight)
if self._norm != 'none': if self._norm in ['right', 'both']:
degs = graph.in_degrees().float().clamp(min=1) degs = graph.in_degrees().float().clamp(min=1)
if self._norm == 'both': if self._norm == 'both':
norm = th.pow(degs, -0.5) norm = th.pow(degs, -0.5)
......
...@@ -34,10 +34,18 @@ class GraphConv(layers.Layer): ...@@ -34,10 +34,18 @@ class GraphConv(layers.Layer):
out_feats : int out_feats : int
Output feature size; i.e., the number of dimensions of :math:`h_i^{(l+1)}`. Output feature size; i.e., the number of dimensions of :math:`h_i^{(l+1)}`.
norm : str, optional norm : str, optional
How to apply the normalizer. If is `'right'`, divide the aggregated messages How to apply the normalizer. Can be one of the following values:
by each node's in-degrees, which is equivalent to averaging the received messages.
If is `'none'`, no normalization is applied. Default is `'both'`, * ``right``, to divide the aggregated messages by each node's in-degrees,
where the :math:`c_{ij}` in the paper is applied. which is equivalent to averaging the received messages.
* ``none``, where no normalization is applied.
* ``both`` (default), where the messages are scaled with :math:`1/c_{ji}` above, equivalent
to symmetric normalization.
* ``left``, to divide the messages sent out from each node by its out-degrees,
equivalent to random walk normalization.
weight : bool, optional weight : bool, optional
If True, apply a linear layer. Otherwise, aggregating the messages If True, apply a linear layer. Otherwise, aggregating the messages
without a weight matrix. without a weight matrix.
...@@ -137,8 +145,8 @@ class GraphConv(layers.Layer): ...@@ -137,8 +145,8 @@ class GraphConv(layers.Layer):
activation=None, activation=None,
allow_zero_in_degree=False): allow_zero_in_degree=False):
super(GraphConv, self).__init__() super(GraphConv, self).__init__()
if norm not in ('none', 'both', 'right'): if norm not in ('none', 'both', 'right', 'left'):
raise DGLError('Invalid norm value. Must be either "none", "both" or "right".' raise DGLError('Invalid norm value. Must be either "none", "both", "right" or "left".'
' But got "{}".'.format(norm)) ' But got "{}".'.format(norm))
self._in_feats = in_feats self._in_feats = in_feats
self._out_feats = out_feats self._out_feats = out_feats
...@@ -230,13 +238,15 @@ class GraphConv(layers.Layer): ...@@ -230,13 +238,15 @@ class GraphConv(layers.Layer):
'suppress the check and let the code run.') 'suppress the check and let the code run.')
feat_src, feat_dst = expand_as_pair(feat, graph) feat_src, feat_dst = expand_as_pair(feat, graph)
if self._norm in ['both', 'left']:
if self._norm == 'both':
degs = tf.clip_by_value(tf.cast(graph.out_degrees(), tf.float32), degs = tf.clip_by_value(tf.cast(graph.out_degrees(), tf.float32),
clip_value_min=1, clip_value_min=1,
clip_value_max=np.inf) clip_value_max=np.inf)
norm = tf.pow(degs, -0.5) if self._norm == 'both':
shp = norm.shape + (1,) * (feat_src.ndim - 1) norm = tf.pow(degs, -0.5)
else:
norm = 1.0 / degs
shp = norm.shape + (1,) * (feat_dst.ndim - 1)
norm = tf.reshape(norm, shp) norm = tf.reshape(norm, shp)
feat_src = feat_src * norm feat_src = feat_src * norm
...@@ -265,7 +275,7 @@ class GraphConv(layers.Layer): ...@@ -265,7 +275,7 @@ class GraphConv(layers.Layer):
if weight is not None: if weight is not None:
rst = tf.matmul(rst, weight) rst = tf.matmul(rst, weight)
if self._norm != 'none': if self._norm in ['both', 'right']:
degs = tf.clip_by_value(tf.cast(graph.in_degrees(), tf.float32), degs = tf.clip_by_value(tf.cast(graph.in_degrees(), tf.float32),
clip_value_min=1, clip_value_min=1,
clip_value_max=np.inf) clip_value_max=np.inf)
......
...@@ -81,7 +81,7 @@ def test_graph_conv(idtype, out_dim): ...@@ -81,7 +81,7 @@ def test_graph_conv(idtype, out_dim):
@parametrize_dtype @parametrize_dtype
@pytest.mark.parametrize('g', get_cases(['homo', 'block-bipartite'], exclude=['zero-degree', 'dglgraph'])) @pytest.mark.parametrize('g', get_cases(['homo', 'block-bipartite'], exclude=['zero-degree', 'dglgraph']))
@pytest.mark.parametrize('norm', ['none', 'both', 'right']) @pytest.mark.parametrize('norm', ['none', 'both', 'right', 'left'])
@pytest.mark.parametrize('weight', [True, False]) @pytest.mark.parametrize('weight', [True, False])
@pytest.mark.parametrize('bias', [False]) @pytest.mark.parametrize('bias', [False])
@pytest.mark.parametrize('out_dim', [1, 2]) @pytest.mark.parametrize('out_dim', [1, 2])
......
...@@ -81,7 +81,7 @@ def test_graph_conv0(out_dim): ...@@ -81,7 +81,7 @@ def test_graph_conv0(out_dim):
@parametrize_dtype @parametrize_dtype
@pytest.mark.parametrize('g', get_cases(['homo', 'bipartite'], exclude=['zero-degree', 'dglgraph'])) @pytest.mark.parametrize('g', get_cases(['homo', 'bipartite'], exclude=['zero-degree', 'dglgraph']))
@pytest.mark.parametrize('norm', ['none', 'both', 'right']) @pytest.mark.parametrize('norm', ['none', 'both', 'right', 'left'])
@pytest.mark.parametrize('weight', [True, False]) @pytest.mark.parametrize('weight', [True, False])
@pytest.mark.parametrize('bias', [True, False]) @pytest.mark.parametrize('bias', [True, False])
@pytest.mark.parametrize('out_dim', [1, 2]) @pytest.mark.parametrize('out_dim', [1, 2])
......
...@@ -74,7 +74,7 @@ def test_graph_conv(out_dim): ...@@ -74,7 +74,7 @@ def test_graph_conv(out_dim):
@parametrize_dtype @parametrize_dtype
@pytest.mark.parametrize('g', get_cases(['homo', 'block-bipartite'], exclude=['zero-degree', 'dglgraph'])) @pytest.mark.parametrize('g', get_cases(['homo', 'block-bipartite'], exclude=['zero-degree', 'dglgraph']))
@pytest.mark.parametrize('norm', ['none', 'both', 'right']) @pytest.mark.parametrize('norm', ['none', 'both', 'right', 'left'])
@pytest.mark.parametrize('weight', [True, False]) @pytest.mark.parametrize('weight', [True, False])
@pytest.mark.parametrize('bias', [True, False]) @pytest.mark.parametrize('bias', [True, False])
@pytest.mark.parametrize('out_dim', [1, 2]) @pytest.mark.parametrize('out_dim', [1, 2])
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment