How to precompute the graph diffusion matrix for GCN?

Hi,

I’m wondering how to precompute the graph diffusion matrix (https://arxiv.org/abs/1911.05485) with DGL. Previously I found this ticket, and my idea is to precompute the diffusion matrix and then apply the GCN on it without using the built-in normalization.

Could you help show me an example to modify the symmetric normalization \tilde{D}^{-\frac{1}{2}}\tilde{A}\tilde{D}^{-\frac{1}{2}} to the diffusion PPR matrix? The closed-form formula is: S^{PPR}=\alpha(I_n - (1-\alpha)D^{-\frac{1}{2}}AD^{-\frac{1}{2}})^{-1} where A should be the unnormalized adjacency matrix (please correct me if I’m wrong with this formula :grinning:).

The code I referred:

import dgl.function as fn
import torch as th
from torch import nn
from torch.nn import init

from dgl.base import DGLError
from dgl.utils import expand_as_pair

class GraphConv(nn.Module):
    r"""
    Parameters
    ----------
    in_feats : int
        Input feature size.
    out_feats : int
        Output feature size.
    bias : bool, optional
        If True, adds a learnable bias to the output. Default: ``True``.
    activation: callable activation function/layer or None, optional
        If not None, applies an activation function to the updated node features.
        Default: ``None``.

    Attributes
    ----------
    weight : torch.Tensor
        The learnable weight tensor.
    bias : torch.Tensor
        The learnable bias tensor.
    """
    def __init__(self,
                 in_feats,
                 out_feats,
                 bias=True,
                 activation=None):
        super(GraphConv, self).__init__()

        self._in_feats = in_feats
        self._out_feats = out_feats
        self.weight = nn.Parameter(th.Tensor(in_feats, out_feats))

        if bias:
            self.bias = nn.Parameter(th.Tensor(out_feats))
        else:
            self.register_parameter('bias', None)

        self.reset_parameters()

        self._activation = activation

    def reset_parameters(self):
        """Reinitialize learnable parameters."""
        if self.weight is not None:
            init.xavier_uniform_(self.weight)
        if self.bias is not None:
            init.zeros_(self.bias)

    def forward(self, graph, feat, eweight):
        r"""Compute graph convolution.

        Notes
        -----
        * Input shape: :math:`(N, *, \text{in_feats})` where * means any number of additional
          dimensions, :math:`N` is the number of nodes.
        * Output shape: :math:`(N, *, \text{out_feats})` where all but the last dimension are
          the same shape as the input.

        Parameters
        ----------
        graph : DGLGraph
            The graph.
        feat : torch.Tensor or pair of torch.Tensor
            If a torch.Tensor is given, it represents the input feature of shape
            :math:`(N, D_{in})`
            where :math:`D_{in}` is size of input feature, :math:`N` is the number of nodes.
            If a pair of torch.Tensor is given, the pair must contain two tensors of shape
            :math:`(N_{in}, D_{in_{src}})` and :math:`(N_{out}, D_{in_{dst}})`.
            Note that in the special case of graph convolutional networks, if a pair of
            tensors is given, the latter element will not participate in computation.
        eweight : torch.Tensor of shape (E, 1)
            Values associated with the edges in the adjacency matrix.

        Returns
        -------
        torch.Tensor
            The output feature
        """
        with graph.local_scope():
            feat_src, feat_dst = expand_as_pair(feat, graph)

            if self._in_feats > self._out_feats:
                # mult W first to reduce the feature size for aggregation.
                feat_src = th.matmul(feat_src, self.weight)
                graph.srcdata['h'] = feat_src
                graph.edata['w'] = eweight
                graph.update_all(fn.u_mul_e('h', 'w', 'm'),
                                            fn.sum('m', 'h'))
                rst = graph.dstdata['h']
            else:
                # aggregate first then mult W
                graph.srcdata['h'] = feat_src
                graph.edata['w'] = eweight
                graph.update_all(fn.u_mul_e('h', 'w', 'm'),
                                            fn.sum('m', 'h'))
                rst = graph.dstdata['h']
                rst = th.matmul(rst, self.weight)

            if self.bias is not None:
                rst = rst + self.bias

            if self._activation is not None:
                rst = self._activation(rst)

            return rst

    def extra_repr(self):
        """Set the extra representation of the module,
        which will come into effect when printing the model.
        """
        summary = 'in={_in_feats}, out={_out_feats}'
        summary += ', normalization={_norm}'
        if '_activation' in self.__dict__:
            summary += ', activation={_activation}'
        return summary.format(**self.__dict__)

# The norm can be computed as follows
in_degs = g.in_degrees().float()
in_norm = th.pow(in_degs, -0.5).unsqueeze(-1)
out_degs = g.out_degrees().float()
out_norm = th.pow(out_degs, -0.5).unsqueeze(-1)
g.ndata['in_norm'] = in_norm
g.ndata['out_norm'] = out_norm
g.apply_edges(fn.u_mul_v('in_norm', 'out_norm', 'eweight'))

The equation should be S^{PPR}=\alpha(I_n - (1-\alpha)D^{-1}A)^{-1} as T=T_{rw} rather than T_{sym}. APPNP is a follow-up paper also from Stephan Gunnemann’s group that propagates node features with personalized pagerank. You can find a DGL example here.

Hi Mufei,

Thank you so much for your timely reply! Since I’m more interested in obtaining the diffused DGLGraph so I’m wondering could I do it in this way:

adj = graph.adj().to_dense().numpy()   # graph is a DGLGraph
d = np.diag(np.sum(a, 1))
dinv = fractional_matrix_power(d, -1)
at = np.matmul(dinv, a)
ppr = alpha * inv((np.eye(a.shape[0]) - (1 - alpha) * at))

when I get this diffusion matrix, could I just create a new DGLGraph based on this diffusion matrix and copy the original graph.ndata to it:

new_graph = dgl.from_scipy(sp_mat = coo_matrix(ppr, dtype=np.float32), eweight_name='eweight')
new_graph.ndata['feat'] = graph.ndata['feat']  # For example, the features

Thank you again for the help! :smiley:

Yes, that looks good to me.