Files
dmlc--dgl/python/dgl/nn/mxnet/conv/relgraphconv.py
T
2026-07-13 13:35:51 +08:00

288 lines
9.8 KiB
Python

"""MXNet module for RelGraphConv"""
# pylint: disable= no-member, arguments-differ, invalid-name
import math
import mxnet as mx
import numpy as np
from mxnet import gluon, nd
from mxnet.gluon import nn
from .... import function as fn
from .. import utils
class RelGraphConv(gluon.Block):
r"""Relational graph convolution layer from `Modeling Relational Data with Graph
Convolutional Networks <https://arxiv.org/abs/1703.06103>`__
It can be described as below:
.. math::
h_i^{(l+1)} = \sigma(\sum_{r\in\mathcal{R}}
\sum_{j\in\mathcal{N}^r(i)}\frac{1}{c_{i,r}}W_r^{(l)}h_j^{(l)}+W_0^{(l)}h_i^{(l)})
where :math:`\mathcal{N}^r(i)` is the neighbor set of node :math:`i` w.r.t. relation
:math:`r`. :math:`c_{i,r}` is the normalizer equal
to :math:`|\mathcal{N}^r(i)|`. :math:`\sigma` is an activation function. :math:`W_0`
is the self-loop weight.
The basis regularization decomposes :math:`W_r` by:
.. math::
W_r^{(l)} = \sum_{b=1}^B a_{rb}^{(l)}V_b^{(l)}
where :math:`B` is the number of bases, :math:`V_b^{(l)}` are linearly combined
with coefficients :math:`a_{rb}^{(l)}`.
The block-diagonal-decomposition regularization decomposes :math:`W_r` into :math:`B`
number of block diagonal matrices. We refer :math:`B` as the number of bases.
The block regularization decomposes :math:`W_r` by:
.. math::
W_r^{(l)} = \oplus_{b=1}^B Q_{rb}^{(l)}
where :math:`B` is the number of bases, :math:`Q_{rb}^{(l)}` are block
bases with shape :math:`R^{(d^{(l+1)}/B)*(d^{l}/B)}`.
Parameters
----------
in_feat : int
Input feature size; i.e, the number of dimensions of :math:`h_j^{(l)}`.
out_feat : int
Output feature size; i.e., the number of dimensions of :math:`h_i^{(l+1)}`.
num_rels : int
Number of relations. .
regularizer : str
Which weight regularizer to use "basis" or "bdd".
"basis" is short for basis-diagonal-decomposition.
"bdd" is short for block-diagonal-decomposition.
num_bases : int, optional
Number of bases. If is none, use number of relations. Default: ``None``.
bias : bool, optional
True if bias is added. Default: ``True``.
activation : callable, optional
Activation function. Default: ``None``.
self_loop : bool, optional
True to include self loop message. Default: ``True``.
low_mem : bool, optional
True to use low memory implementation of relation message passing function. Default: False.
This option trades speed with memory consumption, and will slowdown the forward/backward.
Turn it on when you encounter OOM problem during training or evaluation. Default: ``False``.
dropout : float, optional
Dropout rate. Default: ``0.0``
layer_norm: float, optional
Add layer norm. Default: ``False``
Examples
--------
>>> import dgl
>>> import numpy as np
>>> import mxnet as mx
>>> from mxnet import gluon
>>> from dgl.nn import RelGraphConv
>>>
>>> g = dgl.graph(([0,1,2,3,2,5], [1,2,3,4,0,3]))
>>> feat = mx.nd.ones((6, 10))
>>> conv = RelGraphConv(10, 2, 3, regularizer='basis', num_bases=2)
>>> conv.initialize(ctx=mx.cpu(0))
>>> etype = mx.nd.array(np.array([0,1,2,0,1,2]).astype(np.int64))
>>> res = conv(g, feat, etype)
[[ 0.561324 0.33745846]
[ 0.61585337 0.09992217]
[ 0.561324 0.33745846]
[-0.01557937 0.01227859]
[ 0.61585337 0.09992217]
[ 0.056508 -0.00307822]]
<NDArray 6x2 @cpu(0)>
"""
def __init__(
self,
in_feat,
out_feat,
num_rels,
regularizer="basis",
num_bases=None,
bias=True,
activation=None,
self_loop=True,
low_mem=False,
dropout=0.0,
layer_norm=False,
):
super(RelGraphConv, self).__init__()
self.in_feat = in_feat
self.out_feat = out_feat
self.num_rels = num_rels
self.regularizer = regularizer
self.num_bases = num_bases
if (
self.num_bases is None
or self.num_bases > self.num_rels
or self.num_bases < 0
):
self.num_bases = self.num_rels
self.bias = bias
self.activation = activation
self.self_loop = self_loop
assert (
low_mem is False
), "MXNet currently does not support low-memory implementation."
assert (
layer_norm is False
), "MXNet currently does not support layer norm."
if regularizer == "basis":
# add basis weights
self.weight = self.params.get(
"weight",
shape=(self.num_bases, self.in_feat, self.out_feat),
init=mx.init.Xavier(magnitude=math.sqrt(2.0)),
)
if self.num_bases < self.num_rels:
# linear combination coefficients
self.w_comp = self.params.get(
"w_comp",
shape=(self.num_rels, self.num_bases),
init=mx.init.Xavier(magnitude=math.sqrt(2.0)),
)
# message func
self.message_func = self.basis_message_func
elif regularizer == "bdd":
if in_feat % num_bases != 0 or out_feat % num_bases != 0:
raise ValueError(
"Feature size must be a multiplier of num_bases."
)
# add block diagonal weights
self.submat_in = in_feat // self.num_bases
self.submat_out = out_feat // self.num_bases
# assuming in_feat and out_feat are both divisible by num_bases
self.weight = self.params.get(
"weight",
shape=(
self.num_rels,
self.num_bases * self.submat_in * self.submat_out,
),
init=mx.init.Xavier(magnitude=math.sqrt(2.0)),
)
# message func
self.message_func = self.bdd_message_func
else:
raise ValueError("Regularizer must be either 'basis' or 'bdd'")
# bias
if self.bias:
self.h_bias = self.params.get(
"bias", shape=(out_feat,), init=mx.init.Zero()
)
# weight for self loop
if self.self_loop:
self.loop_weight = self.params.get(
"W_0",
shape=(in_feat, out_feat),
init=mx.init.Xavier(magnitude=math.sqrt(2.0)),
)
self.dropout = nn.Dropout(dropout)
def basis_message_func(self, edges):
"""Message function for basis regularizer"""
ctx = edges.src["h"].context
if self.num_bases < self.num_rels:
# generate all weights from bases
weight = self.weight.data(ctx).reshape(
self.num_bases, self.in_feat * self.out_feat
)
weight = nd.dot(self.w_comp.data(ctx), weight).reshape(
self.num_rels, self.in_feat, self.out_feat
)
else:
weight = self.weight.data(ctx)
msg = utils.bmm_maybe_select(edges.src["h"], weight, edges.data["type"])
if "norm" in edges.data:
msg = msg * edges.data["norm"]
return {"msg": msg}
def bdd_message_func(self, edges):
"""Message function for block-diagonal-decomposition regularizer"""
ctx = edges.src["h"].context
if (
edges.src["h"].dtype in (np.int32, np.int64)
and len(edges.src["h"].shape) == 1
):
raise TypeError(
"Block decomposition does not allow integer ID feature."
)
weight = self.weight.data(ctx)[edges.data["type"], :].reshape(
-1, self.submat_in, self.submat_out
)
node = edges.src["h"].reshape(-1, 1, self.submat_in)
msg = nd.batch_dot(node, weight).reshape(-1, self.out_feat)
if "norm" in edges.data:
msg = msg * edges.data["norm"]
return {"msg": msg}
def forward(self, g, x, etypes, norm=None):
"""
Description
-----------
Forward computation
Parameters
----------
g : DGLGraph
The graph.
feat : mx.ndarray.NDArray
Input node features. Could be either
* :math:`(|V|, D)` dense tensor
* :math:`(|V|,)` int64 vector, representing the categorical values of each
node. It then treat the input feature as an one-hot encoding feature.
etypes : mx.ndarray.NDArray
Edge type tensor. Shape: :math:`(|E|,)`
norm : mx.ndarray.NDArray
Optional edge normalizer tensor. Shape: :math:`(|E|, 1)`.
Returns
-------
mx.ndarray.NDArray
New node features.
"""
assert g.is_homogeneous, (
"not a homogeneous graph; convert it with to_homogeneous "
"and pass in the edge type as argument"
)
with g.local_scope():
g.ndata["h"] = x
g.edata["type"] = etypes
if norm is not None:
g.edata["norm"] = norm
if self.self_loop:
loop_message = utils.matmul_maybe_select(
x, self.loop_weight.data(x.context)
)
# message passing
g.update_all(self.message_func, fn.sum(msg="msg", out="h"))
# apply bias and activation
node_repr = g.ndata["h"]
if self.bias:
node_repr = node_repr + self.h_bias.data(x.context)
if self.self_loop:
node_repr = node_repr + loop_message
if self.activation:
node_repr = self.activation(node_repr)
node_repr = self.dropout(node_repr)
return node_repr