import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.modules.module import Module
from torch.nn.parameter import Parameter
from .. import BaseModel, register_model
class GraphConvolutionBS(Module):
"""
GCN Layer with BN, Self-loop and Res connection.
"""
def __init__(
self,
in_features,
out_features,
activation=lambda x: x,
withbn=True,
withloop=True,
bias=True,
res=False,
):
"""
Initial function.
:param in_features: the input feature dimension.
:param out_features: the output feature dimension.
:param activation: the activation function.
:param withbn: using batch normalization.
:param withloop: using self feature modeling.
:param bias: enable bias.
:param res: enable res connections.
"""
super(GraphConvolutionBS, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.sigma = activation
self.res = res
# Parameter setting.
self.weight = Parameter(torch.FloatTensor(in_features, out_features))
self.self_weight = Parameter(torch.FloatTensor(in_features, out_features)) if withloop else None
self.bn = torch.nn.BatchNorm1d(out_features) if withbn else None
self.bias = Parameter(torch.FloatTensor(out_features)) if bias else None
self.reset_parameters()
def reset_parameters(self):
stdv = 1.0 / math.sqrt(self.weight.size(1))
self.weight.data.uniform_(-stdv, stdv)
if self.self_weight is not None:
stdv = 1.0 / math.sqrt(self.self_weight.size(1))
self.self_weight.data.uniform_(-stdv, stdv)
if self.bias is not None:
self.bias.data.uniform_(-stdv, stdv)
def forward(self, input, adj):
support = torch.mm(input, self.weight)
output = torch.spmm(adj, support)
# Self-loop
output = output + torch.mm(input, self.self_weight) if self.self_weight is not None else output
output = output + self.bias if self.bias is not None else output
# BN
output = self.bn(output) if self.bn is not None else output
# Res
return self.sigma(output) + input if self.res else self.sigma(output)
def __repr__(self):
return self.__class__.__name__ + " (" + str(self.in_features) + " -> " + str(self.out_features) + ")"
class GraphBaseBlock(Module):
"""
The base block for Multi-layer GCN / ResGCN / Dense GCN
"""
def __init__(
self,
in_features,
out_features,
nbaselayer,
withbn=True,
withloop=True,
activation=F.relu,
dropout=True,
aggrmethod="concat",
dense=False,
):
"""
The base block for constructing DeepGCN model.
:param in_features: the input feature dimension.
:param out_features: the hidden feature dimension.
:param nbaselayer: the number of layers in the base block.
:param withbn: using batch normalization in graph convolution.
:param withloop: using self feature modeling in graph convolution.
:param activation: the activation function, default is ReLu.
:param dropout: the dropout ratio.
:param aggrmethod: the aggregation function for baseblock, can be "concat" and "add". For "resgcn", the default
is "add", for others the default is "concat".
:param dense: enable dense connection
"""
super(GraphBaseBlock, self).__init__()
self.in_features = in_features
self.hiddendim = out_features
self.nhiddenlayer = nbaselayer
self.activation = activation
self.aggrmethod = aggrmethod
self.dense = dense
self.dropout = dropout
self.withbn = withbn
self.withloop = withloop
self.hiddenlayers = nn.ModuleList()
self.__makehidden()
if self.aggrmethod == "concat" and dense is False:
self.out_features = in_features + out_features
elif self.aggrmethod == "concat" and dense is True:
self.out_features = in_features + out_features * nbaselayer
elif self.aggrmethod == "add":
if in_features != self.hiddendim:
raise RuntimeError("The dimension of in_features and hiddendim should be matched in add model.")
self.out_features = out_features
elif self.aggrmethod == "nores":
self.out_features = out_features
else:
raise NotImplementedError("The aggregation method only support 'concat','add' and 'nores'.")
def __makehidden(self):
# for i in xrange(self.nhiddenlayer):
for i in range(self.nhiddenlayer):
if i == 0:
layer = GraphConvolutionBS(
self.in_features,
self.hiddendim,
self.activation,
self.withbn,
self.withloop,
)
else:
layer = GraphConvolutionBS(
self.hiddendim,
self.hiddendim,
self.activation,
self.withbn,
self.withloop,
)
self.hiddenlayers.append(layer)
def _doconcat(self, x, subx):
if x is None:
return subx
if self.aggrmethod == "concat":
return torch.cat((x, subx), 1)
elif self.aggrmethod == "add":
return x + subx
elif self.aggrmethod == "nores":
return x
def forward(self, input, adj):
x = input
denseout = None
# Here out is the result in all levels.
for gc in self.hiddenlayers:
denseout = self._doconcat(denseout, x)
x = gc(x, adj)
x = F.dropout(x, self.dropout, training=self.training)
if not self.dense:
return self._doconcat(x, input)
return self._doconcat(x, denseout) if denseout is not None else x
def get_outdim(self):
return self.out_features
def __repr__(self):
return "%s %s (%d - [%d:%d] > %d)" % (
self.__class__.__name__,
self.aggrmethod,
self.in_features,
self.hiddendim,
self.nhiddenlayer,
self.out_features,
)
class MultiLayerGCNBlock(Module):
"""
Muti-Layer GCN with same hidden dimension.
"""
def __init__(
self,
in_features,
out_features,
nbaselayer,
withbn=True,
withloop=True,
activation=F.relu,
dropout=True,
aggrmethod=None,
dense=None,
):
"""
The multiple layer GCN block.
:param in_features: the input feature dimension.
:param out_features: the hidden feature dimension.
:param nbaselayer: the number of layers in the base block.
:param withbn: using batch normalization in graph convolution.
:param withloop: using self feature modeling in graph convolution.
:param activation: the activation function, default is ReLu.
:param dropout: the dropout ratio.
:param aggrmethod: not applied.
:param dense: not applied.
"""
super(MultiLayerGCNBlock, self).__init__()
self.model = GraphBaseBlock(
in_features=in_features,
out_features=out_features,
nbaselayer=nbaselayer,
withbn=withbn,
withloop=withloop,
activation=activation,
dropout=dropout,
dense=False,
aggrmethod="nores",
)
def forward(self, input, adj):
return self.model.forward(input, adj)
def get_outdim(self):
return self.model.get_outdim()
def __repr__(self):
return "%s %s (%d - [%d:%d] > %d)" % (
self.__class__.__name__,
self.aggrmethod,
self.model.in_features,
self.model.hiddendim,
self.model.nhiddenlayer,
self.model.out_features,
)
class ResGCNBlock(Module):
"""
The multiple layer GCN with residual connection block.
"""
def __init__(
self,
in_features,
out_features,
nbaselayer,
withbn=True,
withloop=True,
activation=F.relu,
dropout=True,
aggrmethod=None,
dense=None,
):
"""
The multiple layer GCN with residual connection block.
:param in_features: the input feature dimension.
:param out_features: the hidden feature dimension.
:param nbaselayer: the number of layers in the base block.
:param withbn: using batch normalization in graph convolution.
:param withloop: using self feature modeling in graph convolution.
:param activation: the activation function, default is ReLu.
:param dropout: the dropout ratio.
:param aggrmethod: not applied.
:param dense: not applied.
"""
super(ResGCNBlock, self).__init__()
self.model = GraphBaseBlock(
in_features=in_features,
out_features=out_features,
nbaselayer=nbaselayer,
withbn=withbn,
withloop=withloop,
activation=activation,
dropout=dropout,
dense=False,
aggrmethod="add",
)
def forward(self, input, adj):
return self.model.forward(input, adj)
def get_outdim(self):
return self.model.get_outdim()
def __repr__(self):
return "%s %s (%d - [%d:%d] > %d)" % (
self.__class__.__name__,
self.aggrmethod,
self.model.in_features,
self.model.hiddendim,
self.model.nhiddenlayer,
self.model.out_features,
)
class DenseGCNBlock(Module):
"""
The multiple layer GCN with dense connection block.
"""
def __init__(
self,
in_features,
out_features,
nbaselayer,
withbn=True,
withloop=True,
activation=F.relu,
dropout=True,
aggrmethod="concat",
dense=True,
):
"""
The multiple layer GCN with dense connection block.
:param in_features: the input feature dimension.
:param out_features: the hidden feature dimension.
:param nbaselayer: the number of layers in the base block.
:param withbn: using batch normalization in graph convolution.
:param withloop: using self feature modeling in graph convolution.
:param activation: the activation function, default is ReLu.
:param dropout: the dropout ratio.
:param aggrmethod: the aggregation function for the output. For denseblock, default is "concat".
:param dense: default is True, cannot be changed.
"""
super(DenseGCNBlock, self).__init__()
self.model = GraphBaseBlock(
in_features=in_features,
out_features=out_features,
nbaselayer=nbaselayer,
withbn=withbn,
withloop=withloop,
activation=activation,
dropout=dropout,
dense=True,
aggrmethod=aggrmethod,
)
def forward(self, input, adj):
return self.model.forward(input, adj)
def get_outdim(self):
return self.model.get_outdim()
def __repr__(self):
return "%s %s (%d - [%d:%d] > %d)" % (
self.__class__.__name__,
self.aggrmethod,
self.model.in_features,
self.model.hiddendim,
self.model.nhiddenlayer,
self.model.out_features,
)
class InceptionGCNBlock(Module):
"""
The multiple layer GCN with inception connection block.
"""
def __init__(
self,
in_features,
out_features,
nbaselayer,
withbn=True,
withloop=True,
activation=F.relu,
dropout=True,
aggrmethod="concat",
dense=False,
):
"""
The multiple layer GCN with inception connection block.
:param in_features: the input feature dimension.
:param out_features: the hidden feature dimension.
:param nbaselayer: the number of layers in the base block.
:param withbn: using batch normalization in graph convolution.
:param withloop: using self feature modeling in graph convolution.
:param activation: the activation function, default is ReLu.
:param dropout: the dropout ratio.
:param aggrmethod: the aggregation function for baseblock, can be "concat" and "add". For "resgcn", the default
is "add", for others the default is "concat".
:param dense: not applied. The default is False, cannot be changed.
"""
super(InceptionGCNBlock, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.hiddendim = out_features
self.nbaselayer = nbaselayer
self.activation = activation
self.aggrmethod = aggrmethod
self.dropout = dropout
self.withbn = withbn
self.withloop = withloop
self.midlayers = nn.ModuleList()
self.__makehidden()
if self.aggrmethod == "concat":
self.out_features = in_features + out_features * nbaselayer
elif self.aggrmethod == "add":
if in_features != self.hiddendim:
raise RuntimeError("The dimension of in_features and hiddendim should be matched in 'add' model.")
self.out_features = out_features
else:
raise NotImplementedError("The aggregation method only support 'concat', 'add'.")
def __makehidden(self):
# for j in xrange(self.nhiddenlayer):
for j in range(self.nbaselayer):
reslayer = nn.ModuleList()
# for i in xrange(j + 1):
for i in range(j + 1):
if i == 0:
layer = GraphConvolutionBS(
self.in_features,
self.hiddendim,
self.activation,
self.withbn,
self.withloop,
)
else:
layer = GraphConvolutionBS(
self.hiddendim,
self.hiddendim,
self.activation,
self.withbn,
self.withloop,
)
reslayer.append(layer)
self.midlayers.append(reslayer)
def forward(self, input, adj):
x = input
for reslayer in self.midlayers:
subx = input
for gc in reslayer:
subx = gc(subx, adj)
subx = F.dropout(subx, self.dropout, training=self.training)
x = self._doconcat(x, subx)
return x
def get_outdim(self):
return self.out_features
def _doconcat(self, x, subx):
if self.aggrmethod == "concat":
return torch.cat((x, subx), 1)
elif self.aggrmethod == "add":
return x + subx
def __repr__(self):
return "%s %s (%d - [%d:%d] > %d)" % (
self.__class__.__name__,
self.aggrmethod,
self.in_features,
self.hiddendim,
self.nbaselayer,
self.out_features,
)
class Dense(Module):
"""
Simple Dense layer, Do not consider adj.
"""
def __init__(self, in_features, out_features, activation=lambda x: x, bias=True, res=False):
super(Dense, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.sigma = activation
self.weight = Parameter(torch.FloatTensor(in_features, out_features))
self.res = res
self.bn = nn.BatchNorm1d(out_features)
self.bias = Parameter(torch.FloatTensor(out_features)) if bias else None
self.reset_parameters()
def reset_parameters(self):
stdv = 1.0 / math.sqrt(self.weight.size(1))
self.weight.data.uniform_(-stdv, stdv)
if self.bias is not None:
self.bias.data.uniform_(-stdv, stdv)
def forward(self, input, adj):
output = torch.mm(input, self.weight)
output = output + self.bias if self.bias is not None else output
output = self.bn(output)
return self.sigma(output)
def __repr__(self):
return self.__class__.__name__ + " (" + str(self.in_features) + " -> " + str(self.out_features) + ")"
[docs]@register_model("dropedge_gcn")
class DropEdge_GCN(BaseModel):
"""
DropEdge: Towards Deep Graph Convolutional Networks on Node Classification
Applying DropEdge to GCN @ https://arxiv.org/pdf/1907.10903.pdf
The model for the single kind of deepgcn blocks.
The model architecture likes:
inputlayer(nfeat)--block(nbaselayer, nhid)--...--outputlayer(nclass)--softmax(nclass)
|------ nhidlayer ----|
The total layer is nhidlayer*nbaselayer + 2.
All options are configurable.
Args:
Initial function.
:param nfeat: the input feature dimension.
:param nhid: the hidden feature dimension.
:param nclass: the output feature dimension.
:param nhidlayer: the number of hidden blocks.
:param dropout: the dropout ratio.
:param baseblock: the baseblock type, can be "mutigcn", "resgcn", "densegcn" and "inceptiongcn".
:param inputlayer: the input layer type, can be "gcn", "dense", "none".
:param outputlayer: the input layer type, can be "gcn", "dense".
:param nbaselayer: the number of layers in one hidden block.
:param activation: the activation function, default is ReLu.
:param withbn: using batch normalization in graph convolution.
:param withloop: using self feature modeling in graph convolution.
:param aggrmethod: the aggregation function for baseblock, can be "concat" and "add". For "resgcn", the default
is "add", for others the default is "concat".
"""
[docs] @staticmethod
def add_args(parser):
"""Add model-specific arguments to the parser."""
# fmt: off
parser.add_argument("--num_features", type=int)
parser.add_argument("--num_classes", type=int)
parser.add_argument('--baseblock', default='mutigcn',
help="Choose the model to be trained.( mutigcn, resgcn, densegcn, inceptiongcn)")
parser.add_argument('--inputlayer', default='gcn',
help="The input layer of the model.")
parser.add_argument('--outputlayer', default='gcn',
help="The output layer of the model.")
parser.add_argument('--hidden_size', type=int, default=64,
help='Number of hidden units.')
parser.add_argument('--dropout', type=float, default=0.5,
help='Dropout rate (1 - keep probability).')
parser.add_argument('--withbn', action='store_true', default=False,
help='Enable Bath Norm GCN')
parser.add_argument('--withloop', action="store_true", default=False,
help="Enable loop layer GCN")
parser.add_argument('--nhiddenlayer', type=int, default=1,
help='The number of hidden layers.')
parser.add_argument("--nbaseblocklayer", type=int, default=0,
help="The number of layers in each baseblock")
parser.add_argument("--aggrmethod", default="default",
help="The aggrmethod for the layer aggreation. The options includes add and concat. Only valid in resgcn, densegcn and inceptiongcn")
parser.add_argument("--task_type", default="full", help="The node classification task type (full and semi). Only valid for cora, citeseer and pubmed dataset.")
parser.add_argument("--activation", default=F.relu, help="activiation function")
# fmt: on
[docs] @classmethod
def build_model_from_args(cls, args):
return cls(
args.num_features,
args.hidden_size,
args.num_classes,
args.nhiddenlayer,
args.dropout,
args.baseblock,
args.inputlayer,
args.outputlayer,
args.nbaseblocklayer,
args.activation,
args.withbn,
args.withloop,
args.aggrmethod,
)
def __init__(
self,
nfeat,
nhid,
nclass,
nhidlayer,
dropout,
baseblock,
inputlayer,
outputlayer,
nbaselayer,
activation,
withbn,
withloop,
aggrmethod,
):
super(DropEdge_GCN, self).__init__()
self.dropout = dropout
if baseblock == "resgcn":
self.BASEBLOCK = ResGCNBlock
elif baseblock == "densegcn":
self.BASEBLOCK = DenseGCNBlock
elif baseblock == "mutigcn":
self.BASEBLOCK = MultiLayerGCNBlock
elif baseblock == "inceptiongcn":
self.BASEBLOCK = InceptionGCNBlock
else:
raise NotImplementedError("Current baseblock %s is not supported." % (baseblock))
if inputlayer == "gcn":
# input gc
self.ingc = GraphConvolutionBS(nfeat, nhid, activation, withbn, withloop)
baseblockinput = nhid
elif inputlayer == "none":
self.ingc = lambda x: x
baseblockinput = nfeat
else:
self.ingc = Dense(nfeat, nhid, activation)
baseblockinput = nhid
outactivation = lambda x: x # noqa E731
if outputlayer == "gcn":
self.outgc = GraphConvolutionBS(baseblockinput, nclass, outactivation, withbn, withloop)
# elif outputlayer == "none": #here can not be none
# self.outgc = lambda x: x
else:
self.outgc = Dense(nhid, nclass, activation)
# hidden layer
self.midlayer = nn.ModuleList()
# Dense is not supported now.
# for i in xrange(nhidlayer):
for i in range(nhidlayer):
gcb = self.BASEBLOCK(
in_features=baseblockinput,
out_features=nhid,
nbaselayer=nbaselayer,
withbn=withbn,
withloop=withloop,
activation=activation,
dropout=dropout,
dense=False,
aggrmethod=aggrmethod,
)
self.midlayer.append(gcb)
baseblockinput = gcb.get_outdim()
# output gc
outactivation = lambda x: x # noqa E731 we donot need nonlinear activation here.
self.outgc = GraphConvolutionBS(baseblockinput, nclass, outactivation, withbn, withloop)
self.reset_parameters()
[docs] def reset_parameters(self):
pass
[docs] def forward(self, fea, adj):
def fix_adj_format(input, edge_index, edge_attr=None):
if edge_attr is None:
edge_attr = torch.ones(edge_index.shape[1]).float().to(input.device)
adj = torch.sparse_coo_tensor(
edge_index,
edge_attr,
(input.shape[0], input.shape[0]),
).to(input.device)
return adj
# get correct format
adj = fix_adj_format(fea, adj)
# input
x = self.ingc(fea, adj)
x = F.dropout(x, self.dropout, training=self.training)
# mid block connections
# for i in xrange(len(self.midlayer)):
for i in range(len(self.midlayer)):
midgc = self.midlayer[i]
x = midgc(x, adj)
# output, no relu and dropput here.
x = self.outgc(x, adj)
x = F.log_softmax(x, dim=1)
return x
[docs] def predict(self, data):
return self.forward(data.x, data.edge_index)