defforward(self, x, edge_index): # x has shape [N, in_channels] # edge_index has shape [2, E]
# Step 1: Add self-loops to the adjacency matrix. # Adds a self-loop (i,i)∈E to every node i∈V in the graph given by edge_index. # In case the graph is weighted, self-loops will be added with edge weights denoted by fill_value. edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
## download data to current directory #! wget https://github.com/kimiyoung/planetoid/raw/master/data/ind.cora.x
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
from torch_geometric.datasets import Planetoid
dataset = Planetoid(root='./dataset/Cora', name='Cora') data = dataset[0] # GCNConv: #in_channels: dimension of input vector of linear layer # out_channels: dimension of output vector of linear layer #Note: the linear transform is performed before message passing to reduce the dimension of node representation # After message passing, the amount of nodes doesn't change net = GCNConv(data.num_features, 64)
# data.x: a matrix with each row representing the data in a node # data.edge_index: matrix with shape [2, number of edges], each column representing edge from node to another node, value=index of node h_nodes = net(data.x, data.edge_index) print(h_nodes.shape)
from torch_geometric.datasets import Planetoid import torch from torch_geometric.nn import MessagePassing from torch_geometric.utils import add_self_loops, degree from torch_sparse import SparseTensor
# Step 4-5: Start propagating messages. # Convert edge index to a sparse adjacency matrix representation, with row = from nodes, col = to nodes, value = 0 or 1 indicating if # two nodes are adjacent. adjmat = SparseTensor(row=edge_index[0], col=edge_index[1], value=torch.ones(edge_index.shape[1])) #print("Adjacency matrix:",adjmat) # 此处传的不再是edge_idex,而是SparseTensor类型的Adjancency Matrix return self.propagate(adjmat, x=x, norm=norm, deg=deg.view((-1, 1)))
from torch_geometric.datasets import Planetoid import torch from torch import nn, Tensor from torch_geometric.nn import MessagePassing from torch_geometric.utils import add_self_loops, degree from torch_sparse import SparseTensor, matmul
defpropagate(self, edge_index, size=None, **kwargs): # I just copy the source copy from PyG website r"""The initial call to start propagating messages. Args: edge_index (Tensor or SparseTensor): A :obj:`torch.LongTensor` or a :obj:`torch_sparse.SparseTensor` that defines the underlying graph connectivity/message passing flow. :obj:`edge_index` holds the indices of a general (sparse) assignment matrix of shape :obj:`[N, M]`. If :obj:`edge_index` is of type :obj:`torch.LongTensor`, its shape must be defined as :obj:`[2, num_messages]`, where messages from nodes in :obj:`edge_index[0]` are sent to nodes in :obj:`edge_index[1]` (in case :obj:`flow="source_to_target"`). If :obj:`edge_index` is of type :obj:`torch_sparse.SparseTensor`, its sparse indices :obj:`(row, col)` should relate to :obj:`row = edge_index[1]` and :obj:`col = edge_index[0]`. The major difference between both formats is that we need to input the *transposed* sparse adjacency matrix into :func:`propagate`. size (tuple, optional): The size :obj:`(N, M)` of the assignment matrix in case :obj:`edge_index` is a :obj:`LongTensor`. If set to :obj:`None`, the size will be automatically inferred and assumed to be quadratic. This argument is ignored in case :obj:`edge_index` is a :obj:`torch_sparse.SparseTensor`. (default: :obj:`None`) **kwargs: Any additional data which is needed to construct and aggregate messages, and to update node embeddings. """ size = self.__check_input__(edge_index, size)
# Run "fused" message and aggregation (if applicable). if (isinstance(edge_index, SparseTensor) and self.fuse andnot self.__explain__): coll_dict = self.__collect__(self.__fused_user_args__, edge_index, size, kwargs) print("Using self-defined message-passing") msg_aggr_kwargs = self.inspector.distribute( 'message_and_aggregate', coll_dict) out = self.message_and_aggregate(edge_index, **msg_aggr_kwargs)
# Otherwise, run both functions in separation. elif isinstance(edge_index, Tensor) ornot self.fuse: coll_dict = self.__collect__(self.__user_args__, edge_index, size, kwargs)
msg_kwargs = self.inspector.distribute('message', coll_dict) #print("Message kwargs: ",msg_kwargs) out = self.message(**msg_kwargs)
# For `GNNExplainer`, we require a separate message and aggregate # procedure since this allows us to inject the `edge_mask` into the # message passing computation scheme. if self.__explain__: edge_mask = self.__edge_mask__.sigmoid() # Some ops add self-loops to `edge_index`. We need to do the # same for `edge_mask` (but do not train those). if out.size(self.node_dim) != edge_mask.size(0): loop = edge_mask.new_ones(size[0]) edge_mask = torch.cat([edge_mask, loop], dim=0) assert out.size(self.node_dim) == edge_mask.size(0) out = out * edge_mask.view([-1] + [1] * (out.dim() - 1))
aggr_kwargs = self.inspector.distribute('aggregate', coll_dict) out = self.aggregate(out, **aggr_kwargs)
update_kwargs = self.inspector.distribute('update', coll_dict) return self.update(out, **update_kwargs) defforward(self, x, edge_index): # x has shape [N, in_channels] # edge_index has shape [2, E]
# Step 1: Add self-loops to the adjacency matrix. edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
from torch_geometric.datasets import Planetoid import torch from torch import nn, Tensor from torch_geometric.nn import MessagePassing from torch_geometric.utils import add_self_loops, degree from torch_sparse import SparseTensor, matmul
defmessage(self, x_j, deg_i,deg_j): # Accoding to __collect__ function # in https://github.com/rusty1s/pytorch_geometric/blob/master/torch_geometric/nn/conv/message_passing.py # when flow = source_to_target # i= 1, j=0, edge_index_i = edge_index[1] = target, so # deg_i is degree of target node, and x_i is target node data # deg_j is degree of source node and x_j is source # x_j has shape [E, out_channels] # deg_i has shape [E, 1] # Step 3: Normalize node features. print("--message is called--") print("x_j: ",x_j.shape) print("degree: ", deg_i.shape) print("degree: ",deg_j.shape) print() # check if degrees of source nodes and degrees of target nodes are equal print(torch.eq(deg_i, deg_j).all()) # compute normalization deg_i = deg_i.pow(-0.5) deg_j = deg_j.pow(-0.5) norm = deg_i * deg_j return norm.view(-1, 1) * x_j defaggregate(self, inputs, index, ptr, dim_size): #from __collect__() function we know that # when flow = source_to_target # out['index'] = out['edge_index_i'] -> input index = edge_index[i] = edge_index[1] = index of target node # inputs: embedding vectors of source nodes # inputs: the outputs from message function, the normalized source node embeding with shape [E, dim of embedding] print("--aggregate` is called--") print('self.aggr:', self.aggr) print('ptr: ', ptr) print('dim_size: ',dim_size) print("inputs: ", inputs.shape) print("index: ",index.shape, len(index.unique())) print() uni_idx = index.unique() uni_idx.sort() res= [] # find all unique target node index # for each target node, aggregate(sum or mean ) the information from source node to the target node # and obtain target node embedding for i in uni_idx: # i is the index of target node neighbors = inputs[index == i] # aggregate along different vectors of different nodes if self.aggr=="mean": agg_res = neighbors.mean(axis=0) else: agg_res = neighbors.sum(axis=0) res.append(agg_res) res = torch.stack(res) return res dataset = Planetoid(root='dataset/Cora', name='Cora') data = dataset[0]
from torch_geometric.datasets import Planetoid import torch from torch import nn, Tensor from torch_geometric.nn import MessagePassing from torch_geometric.utils import add_self_loops, degree from torch_sparse import SparseTensor, matmul
defpropagate(self, edge_index, size=None, **kwargs): # I just copy the source copy from PyG website r"""The initial call to start propagating messages. Args: edge_index (Tensor or SparseTensor): A :obj:`torch.LongTensor` or a :obj:`torch_sparse.SparseTensor` that defines the underlying graph connectivity/message passing flow. :obj:`edge_index` holds the indices of a general (sparse) assignment matrix of shape :obj:`[N, M]`. If :obj:`edge_index` is of type :obj:`torch.LongTensor`, its shape must be defined as :obj:`[2, num_messages]`, where messages from nodes in :obj:`edge_index[0]` are sent to nodes in :obj:`edge_index[1]` (in case :obj:`flow="source_to_target"`). If :obj:`edge_index` is of type :obj:`torch_sparse.SparseTensor`, its sparse indices :obj:`(row, col)` should relate to :obj:`row = edge_index[1]` and :obj:`col = edge_index[0]`. The major difference between both formats is that we need to input the *transposed* sparse adjacency matrix into :func:`propagate`. size (tuple, optional): The size :obj:`(N, M)` of the assignment matrix in case :obj:`edge_index` is a :obj:`LongTensor`. If set to :obj:`None`, the size will be automatically inferred and assumed to be quadratic. This argument is ignored in case :obj:`edge_index` is a :obj:`torch_sparse.SparseTensor`. (default: :obj:`None`) **kwargs: Any additional data which is needed to construct and aggregate messages, and to update node embeddings. """ size = self.__check_input__(edge_index, size)
# Run "fused" message and aggregation (if applicable). if (isinstance(edge_index, SparseTensor) and self.fuse andnot self.__explain__): coll_dict = self.__collect__(self.__fused_user_args__, edge_index, size, kwargs) print("Using self-defined message-passing") msg_aggr_kwargs = self.inspector.distribute( 'message_and_aggregate', coll_dict) out = self.message_and_aggregate(edge_index, **msg_aggr_kwargs)
# Otherwise, run both functions in separation. elif isinstance(edge_index, Tensor) ornot self.fuse: coll_dict = self.__collect__(self.__user_args__, edge_index, size, kwargs)
msg_kwargs = self.inspector.distribute('message', coll_dict) #print("Message kwargs: ",msg_kwargs) out = self.message(**msg_kwargs)
# For `GNNExplainer`, we require a separate message and aggregate # procedure since this allows us to inject the `edge_mask` into the # message passing computation scheme. if self.__explain__: edge_mask = self.__edge_mask__.sigmoid() # Some ops add self-loops to `edge_index`. We need to do the # same for `edge_mask` (but do not train those). if out.size(self.node_dim) != edge_mask.size(0): loop = edge_mask.new_ones(size[0]) edge_mask = torch.cat([edge_mask, loop], dim=0) assert out.size(self.node_dim) == edge_mask.size(0) out = out * edge_mask.view([-1] + [1] * (out.dim() - 1))
aggr_kwargs = self.inspector.distribute('aggregate', coll_dict) out = self.aggregate(out, **aggr_kwargs)
update_kwargs = self.inspector.distribute('update', coll_dict) return self.update(out, **update_kwargs) defforward(self, x, edge_index): # x has shape [N, in_channels] # edge_index has shape [2, E]
# Step 1: Add self-loops to the adjacency matrix. edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
defmessage(self, x_j, deg_i,deg_j): # Accoding to __collect__ function # in https://github.com/rusty1s/pytorch_geometric/blob/master/torch_geometric/nn/conv/message_passing.py # when flow = source_to_target # i= 1, j=0, edge_index_i = edge_index[1] = target, so # deg_i is degree of target node, and x_i is target node data # deg_j is degree of source node and x_j is source # x_j has shape [E, out_channels] # deg_i has shape [E, 1] # Step 3: Normalize node features. print("--message is called--") print("x_j: ",x_j.shape) print("degree: ", deg_i.shape) print("degree: ",deg_j.shape) print() # check if degrees of source nodes and degrees of target nodes are equal print(torch.eq(deg_i, deg_j).all()) # compute normalization deg_i = deg_i.pow(-0.5) deg_j = deg_j.pow(-0.5) norm = deg_i * deg_j return norm.view(-1, 1) * x_j defaggregate(self, inputs, index, ptr, dim_size): #from __collect__() function we know that # when flow = source_to_target # out['index'] = out['edge_index_i'] -> input index = edge_index[i] = edge_index[1] = index of target node # inputs: embedding vectors of source nodes # inputs: the outputs from message function, the normalized source node embeding with shape [E, dim of embedding] print("--aggregate` is called--") print('self.aggr:', self.aggr) print('ptr: ', ptr) print('dim_size: ',dim_size) print("inputs: ", inputs.shape) print("index: ",index.shape, len(index.unique())) print() uni_idx = index.unique() uni_idx.sort() res= [] # find all unique target node index # for each target node, aggregate(sum or mean ) the information from source node to the target node # and obtain target node embedding for i in uni_idx: # i is the index of target node neighbors = inputs[index == i] # aggregate along different vectors of different nodes if self.aggr=="mean": agg_res = neighbors.mean(axis=0) else: agg_res = neighbors.sum(axis=0) res.append(agg_res) res = torch.stack(res) return res defupdate(self,inputs, deg ): print("--update func is called--") return self.lin2(inputs)
dataset = Planetoid(root='dataset/Cora', name='Cora') data = dataset[0]
from torch_geometric.datasets import Planetoid import torch from torch import nn, Tensor from torch_geometric.nn import MessagePassing from torch_geometric.utils import add_self_loops, degree from torch_sparse import SparseTensor, matmul
defpropagate(self, edge_index, size=None, **kwargs): # I just copy the source copy from PyG website r"""The initial call to start propagating messages. Args: edge_index (Tensor or SparseTensor): A :obj:`torch.LongTensor` or a :obj:`torch_sparse.SparseTensor` that defines the underlying graph connectivity/message passing flow. :obj:`edge_index` holds the indices of a general (sparse) assignment matrix of shape :obj:`[N, M]`. If :obj:`edge_index` is of type :obj:`torch.LongTensor`, its shape must be defined as :obj:`[2, num_messages]`, where messages from nodes in :obj:`edge_index[0]` are sent to nodes in :obj:`edge_index[1]` (in case :obj:`flow="source_to_target"`). If :obj:`edge_index` is of type :obj:`torch_sparse.SparseTensor`, its sparse indices :obj:`(row, col)` should relate to :obj:`row = edge_index[1]` and :obj:`col = edge_index[0]`. The major difference between both formats is that we need to input the *transposed* sparse adjacency matrix into :func:`propagate`. size (tuple, optional): The size :obj:`(N, M)` of the assignment matrix in case :obj:`edge_index` is a :obj:`LongTensor`. If set to :obj:`None`, the size will be automatically inferred and assumed to be quadratic. This argument is ignored in case :obj:`edge_index` is a :obj:`torch_sparse.SparseTensor`. (default: :obj:`None`) **kwargs: Any additional data which is needed to construct and aggregate messages, and to update node embeddings. """ size = self.__check_input__(edge_index, size)
# Run "fused" message and aggregation (if applicable). if (isinstance(edge_index, SparseTensor) and self.fuse andnot self.__explain__): coll_dict = self.__collect__(self.__fused_user_args__, edge_index, size, kwargs) #print("Using self-defined message-passing") msg_aggr_kwargs = self.inspector.distribute( 'message_and_aggregate', coll_dict) out = self.message_and_aggregate(edge_index, **msg_aggr_kwargs)
# Otherwise, run both functions in separation. elif isinstance(edge_index, Tensor) ornot self.fuse: coll_dict = self.__collect__(self.__user_args__, edge_index, size, kwargs)
msg_kwargs = self.inspector.distribute('message', coll_dict) #print("Message kwargs: ",msg_kwargs) out = self.message(**msg_kwargs)
# For `GNNExplainer`, we require a separate message and aggregate # procedure since this allows us to inject the `edge_mask` into the # message passing computation scheme. if self.__explain__: edge_mask = self.__edge_mask__.sigmoid() # Some ops add self-loops to `edge_index`. We need to do the # same for `edge_mask` (but do not train those). if out.size(self.node_dim) != edge_mask.size(0): loop = edge_mask.new_ones(size[0]) edge_mask = torch.cat([edge_mask, loop], dim=0) assert out.size(self.node_dim) == edge_mask.size(0) out = out * edge_mask.view([-1] + [1] * (out.dim() - 1))
aggr_kwargs = self.inspector.distribute('aggregate', coll_dict) out = self.aggregate(out, **aggr_kwargs)
update_kwargs = self.inspector.distribute('update', coll_dict) return self.update(out, **update_kwargs) defforward(self, x, edge_index): # x has shape [N, in_channels] # edge_index has shape [2, E]
# Step 1: Add self-loops to the adjacency matrix. edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
defmessage(self, x_j, deg_i,deg_j): # Accoding to __collect__ function # in https://github.com/rusty1s/pytorch_geometric/blob/master/torch_geometric/nn/conv/message_passing.py # when flow = source_to_target # i= 1, j=0, edge_index_i = edge_index[1] = target, so # deg_i is degree of target node, and x_i is target node data # deg_j is degree of source node and x_j is source # x_j has shape [E, out_channels] # deg_i has shape [E, 1] # Step 3: Normalize node features. print("--message is called--") print("x_j: ",x_j.shape) print("degree: ", deg_i.shape) print("degree: ",deg_j.shape) print() # check if degrees of source nodes and degrees of target nodes are equal print(torch.eq(deg_i, deg_j).all()) # compute normalization deg_i = deg_i.pow(-0.5) deg_j = deg_j.pow(-0.5) norm = deg_i * deg_j return norm.view(-1, 1) * x_j defaggregate(self, inputs, index, ptr, dim_size): #from __collect__() function we know that # when flow = source_to_target # out['index'] = out['edge_index_i'] -> input index = edge_index[i] = edge_index[1] = index of target node # inputs: embedding vectors of source nodes # inputs: the outputs from message function, the normalized source node embeding with shape [E, dim of embedding] print("--aggregate` is called--") print('self.aggr:', self.aggr) print('ptr: ', ptr) print('dim_size: ',dim_size) print("inputs: ", inputs.shape) print("index: ",index.shape, len(index.unique())) print() uni_idx = index.unique() uni_idx.sort() res= [] # find all unique target node index # for each target node, aggregate(sum or mean ) the information from source node to the target node # and obtain target node embedding for i in uni_idx: # i is the index of target node neighbors = inputs[index == i] # aggregate along different vectors of different nodes if self.aggr=="mean": agg_res = neighbors.mean(axis=0) else: agg_res = neighbors.sum(axis=0) res.append(agg_res) res = torch.stack(res) return res defmessage_and_aggregate(self, adj_t, x_j, index,deg_i, deg_j): # note: # adj_t: adjacency matrix # norm: normalization coefficient 1/sqrt(deg_i)*sqrt(deg_j) # number of '1' in adj_t = length of norm ## Print something to debug #print('`message_and_aggregate` is called') #print("adj_t: ",adj_t) #print("deg:", deg) print("--message_and_aggregate is called --")
# Step3: compute normalization deg_i = deg_i.pow(-0.5) deg_j = deg_j.pow(-0.5) norm = deg_i * deg_j # Step4: compute normalized message inputs = norm.view(-1, 1) * x_j # Step5: aggregate function sum uni_idx = index.unique() uni_idx.sort() res= [] # find all unique target node index # for each target node, aggregate(sum or mean ) the information from source node to the target node # and obtain target node embedding for i in uni_idx: # i is the index of target node neighbors = inputs[index == i] # aggregate along different vectors of different nodes if self.aggr=="mean": agg_res = neighbors.mean(axis=0) else: agg_res = neighbors.sum(axis=0) res.append(agg_res) res = torch.stack(res) return res defupdate(self,inputs, deg ): print("--update func is called--") return self.lin2(inputs)
dataset = Planetoid(root='dataset/Cora', name='Cora') data = dataset[0]
from torch_geometric.datasets import Planetoid import torch from torch import nn, Tensor from torch_geometric.nn import MessagePassing from torch_geometric.utils import add_self_loops, degree from torch_sparse import SparseTensor, matmul
defpropagate(self, edge_index, size=None, **kwargs): # I just copy the source copy from PyG website r"""The initial call to start propagating messages. Args: edge_index (Tensor or SparseTensor): A :obj:`torch.LongTensor` or a :obj:`torch_sparse.SparseTensor` that defines the underlying graph connectivity/message passing flow. :obj:`edge_index` holds the indices of a general (sparse) assignment matrix of shape :obj:`[N, M]`. If :obj:`edge_index` is of type :obj:`torch.LongTensor`, its shape must be defined as :obj:`[2, num_messages]`, where messages from nodes in :obj:`edge_index[0]` are sent to nodes in :obj:`edge_index[1]` (in case :obj:`flow="source_to_target"`). If :obj:`edge_index` is of type :obj:`torch_sparse.SparseTensor`, its sparse indices :obj:`(row, col)` should relate to :obj:`row = edge_index[1]` and :obj:`col = edge_index[0]`. The major difference between both formats is that we need to input the *transposed* sparse adjacency matrix into :func:`propagate`. size (tuple, optional): The size :obj:`(N, M)` of the assignment matrix in case :obj:`edge_index` is a :obj:`LongTensor`. If set to :obj:`None`, the size will be automatically inferred and assumed to be quadratic. This argument is ignored in case :obj:`edge_index` is a :obj:`torch_sparse.SparseTensor`. (default: :obj:`None`) **kwargs: Any additional data which is needed to construct and aggregate messages, and to update node embeddings. """ size = self.__check_input__(edge_index, size)
# Run "fused" message and aggregation (if applicable). if (isinstance(edge_index, SparseTensor) and self.fuse andnot self.__explain__): coll_dict = self.__collect__(self.__fused_user_args__, edge_index, size, kwargs) print("Using self-defined message-passing") msg_aggr_kwargs = self.inspector.distribute( 'message_and_aggregate', coll_dict) out = self.message_and_aggregate(edge_index, **msg_aggr_kwargs)
# Otherwise, run both functions in separation. elif isinstance(edge_index, Tensor) ornot self.fuse: coll_dict = self.__collect__(self.__user_args__, edge_index, size, kwargs)
msg_kwargs = self.inspector.distribute('message', coll_dict) out = self.message(**msg_kwargs)
# For `GNNExplainer`, we require a separate message and aggregate # procedure since this allows us to inject the `edge_mask` into the # message passing computation scheme. if self.__explain__: edge_mask = self.__edge_mask__.sigmoid() # Some ops add self-loops to `edge_index`. We need to do the # same for `edge_mask` (but do not train those). if out.size(self.node_dim) != edge_mask.size(0): loop = edge_mask.new_ones(size[0]) edge_mask = torch.cat([edge_mask, loop], dim=0) assert out.size(self.node_dim) == edge_mask.size(0) out = out * edge_mask.view([-1] + [1] * (out.dim() - 1))
aggr_kwargs = self.inspector.distribute('aggregate', coll_dict) out = self.aggregate(out, **aggr_kwargs)
update_kwargs = self.inspector.distribute('update', coll_dict) return self.update(out, **update_kwargs) defforward(self, x, edge_index): # x has shape [N, in_channels] # edge_index has shape [2, E]
# Step 1: Add self-loops to the adjacency matrix. edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))