项目文件夹

文件
Chen Sirui d04d59eef9 [Example] Temporal Graph Neural Network (#2636)
* Add hgat example

* Add experiment

* Clean code

* clear the code

* Add index in README

* Add index in README

* Add index in README

* Add index in README

* Add index in README

* Add index in README

* Change the code title and folder name

* Ready to merge

* Prepare for rebase and change message passing function

* use git ignore to handle empty file

* change file permission to resolve empty file

* Change permission

* change file mode

* Finish Coding

* working code cpu

* pyg compare

* Accelerate with batching

* FastMode Enabled

* update readme

* Update README.md

* refractor code

* Fix Bug

* add a simple temporal sampling method

* test results

* add train speed result

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* fix bug

* Fixed Grammar and Format Issue

Co-authored-by: Chen <chesirui@3c22fbe5458c.ant.amazon.com>
Co-authored-by: Tianjun Xiao <xiaotj1990327@gmail.com>
Co-authored-by: Ubuntu <ubuntu@ip-172-31-4-63.ap-northeast-1.compute.internal>
Co-authored-by: WangXuhongCN <wangxuhongcn@163.com>
Co-authored-by: WangXuhongCN <34096632+WangXuhongCN@users.noreply.github.com>
2021-03-23 08:57:21 +08:00

443 行
15 KiB
Python

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import dgl
from dgl.base import DGLError
from dgl.ops import edge_softmax
import dgl.function as fn
class MergeLayer(nn.Module):
"""Merge two tensor into one
Which is useful as skip connection in Merge GAT's input with output
Parameter
----------
dim1 : int
dimension of first input tensor
dim2 : int
dimension of second input tensor
dim3 : int
hidden dimension after first merging
dim4 : int
output dimension
Example
----------
>>> merger = MergeLayer(10,10,10,5)
>>> input1 = torch.ones(4,10)
>>> input2 = torch.ones(4,10)
>>> merger(input1,input2)
tensor([[-0.1578, 0.1842, 0.2373, 1.2656, 1.0362],
[-0.1578, 0.1842, 0.2373, 1.2656, 1.0362],
[-0.1578, 0.1842, 0.2373, 1.2656, 1.0362],
[-0.1578, 0.1842, 0.2373, 1.2656, 1.0362]],
grad_fn=<AddmmBackward>)
"""
def __init__(self, dim1, dim2, dim3, dim4):
super(MergeLayer, self).__init__()
self.fc1 = torch.nn.Linear(dim1 + dim2, dim3)
self.fc2 = torch.nn.Linear(dim3, dim4)
self.act = torch.nn.ReLU()
torch.nn.init.xavier_normal_(self.fc1.weight)
torch.nn.init.xavier_normal_(self.fc2.weight)
def forward(self, x1, x2):
x = torch.cat([x1, x2], dim=1)
h = self.act(self.fc1(x))
return self.fc2(h)
class MsgLinkPredictor(nn.Module):
"""Predict Pair wise link from pos subg and neg subg
use message passing.
Use Two layer MLP on edge to predict the link probability
Parameters
----------
embed_dim : int
dimension of each each feature's embedding
Example
----------
>>> linkpred = MsgLinkPredictor(10)
>>> pos_g = dgl.graph(([0,1,2,3,4],[1,2,3,4,0]))
>>> neg_g = dgl.graph(([0,1,2,3,4],[2,1,4,3,0]))
>>> x = torch.ones(5,10)
>>> linkpred(x,pos_g,neg_g)
(tensor([[0.0902],
[0.0902],
[0.0902],
[0.0902],
[0.0902]], grad_fn=<AddmmBackward>),
tensor([[0.0902],
[0.0902],
[0.0902],
[0.0902],
[0.0902]], grad_fn=<AddmmBackward>))
"""
def __init__(self, emb_dim):
super(MsgLinkPredictor, self).__init__()
self.src_fc = nn.Linear(emb_dim, emb_dim)
self.dst_fc = nn.Linear(emb_dim, emb_dim)
self.out_fc = nn.Linear(emb_dim, 1)
def link_pred(self, edges):
src_hid = self.src_fc(edges.src['embedding'])
dst_hid = self.dst_fc(edges.dst['embedding'])
score = F.relu(src_hid+dst_hid)
score = self.out_fc(score)
return {'score': score}
def forward(self, x, pos_g, neg_g):
# Local Scope?
pos_g.ndata['embedding'] = x
neg_g.ndata['embedding'] = x
pos_g.apply_edges(self.link_pred)
neg_g.apply_edges(self.link_pred)
pos_escore = pos_g.edata['score']
neg_escore = neg_g.edata['score']
return pos_escore, neg_escore
class TimeEncode(nn.Module):
"""Use finite fourier series with different phase and frequency to encode
time different between two event
..math::
\Phi(t) = [\cos(\omega_0t+\psi_0),\cos(\omega_1t+\psi_1),...,\cos(\omega_nt+\psi_n)]
Parameter
----------
dimension : int
Length of the fourier series. The longer it is ,
the more timescale information it can capture
Example
----------
>>> tecd = TimeEncode(10)
>>> t = torch.tensor([[1]])
>>> tecd(t)
tensor([[[0.5403, 0.9950, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000,
1.0000, 1.0000]]], dtype=torch.float64, grad_fn=<CosBackward>)
"""
def __init__(self, dimension):
super(TimeEncode, self).__init__()
self.dimension = dimension
self.w = torch.nn.Linear(1, dimension)
self.w.weight = torch.nn.Parameter((torch.from_numpy(1 / 10 ** np.linspace(0, 9, dimension)))
.double().reshape(dimension, -1))
self.w.bias = torch.nn.Parameter(torch.zeros(dimension).double())
def forward(self, t):
t = t.unsqueeze(dim=2)
output = torch.cos(self.w(t))
return output
class MemoryModule(nn.Module):
"""Memory module as well as update interface
The memory module stores both historical representation in last_update_t
Parameters
----------
n_node : int
number of node of the entire graph
hidden_dim : int
dimension of memory of each node
Example
----------
Please refers to examples/pytorch/tgn/tgn.py;
examples/pytorch/tgn/train.py
"""
def __init__(self, n_node, hidden_dim):
super(MemoryModule, self).__init__()
self.n_node = n_node
self.hidden_dim = hidden_dim
self.reset_memory()
def reset_memory(self):
self.last_update_t = nn.Parameter(torch.zeros(
self.n_node).float(), requires_grad=False)
self.memory = nn.Parameter(torch.zeros(
(self.n_node, self.hidden_dim)).float(), requires_grad=False)
def backup_memory(self):
"""
Return a deep copy of memory state and last_update_t
For test new node, since new node need to use memory upto validation set
After validation, memory need to be backed up before run test set without new node
so finally, we can use backup memory to update the new node test set
"""
return self.memory.clone(), self.last_update_t.clone()
def restore_memory(self, memory_backup):
"""Restore the memory from validation set
Parameters
----------
memory_backup : (memory,last_update_t)
restore memory based on input tuple
"""
self.memory = memory_backup[0].clone()
self.last_update_t = memory_backup[1].clone()
# Which is used for attach to subgraph
def get_memory(self, node_idxs):
return self.memory[node_idxs, :]
# When the memory need to be updated
def set_memory(self, node_idxs, values):
self.memory[node_idxs, :] = values
def set_last_update_t(self, node_idxs, values):
self.last_update_t[node_idxs] = values
# For safety check
def get_last_update(self, node_idxs):
return self.last_update_t[node_idxs]
def detach_memory(self):
"""
Disconnect the memory from computation graph to prevent gradient be propagated multiple
times
"""
self.memory.detach_()
class MemoryOperation(nn.Module):
""" Memory update using message passing manner, update memory based on positive
pair graph of each batch with recurrent module GRU or RNN
Message function
..math::
m_i(t) = concat(memory_i(t^-),TimeEncode(t),v_i(t))
v_i is node feature at current time stamp
Aggregation function
..math::
\bar{m}_i(t) = last(m_i(t_1),...,m_i(t_b))
Update function
..math::
memory_i(t) = GRU(\bar{m}_i(t),memory_i(t-1))
Parameters
----------
updater_type : str
indicator string to specify updater
'rnn' : use Vanilla RNN as updater
'gru' : use GRU as updater
memory : MemoryModule
memory content for update
e_feat_dim : int
dimension of edge feature
temporal_dim : int
length of fourier series for time encoding
Example
----------
Please refers to examples/pytorch/tgn/tgn.py
"""
def __init__(self, updater_type, memory, e_feat_dim, temporal_dim):
super(MemoryOperation, self).__init__()
updater_dict = {'gru': nn.GRUCell, 'rnn': nn.RNNCell}
self.memory = memory
memory_dim = self.memory.hidden_dim
self.message_dim = memory_dim+memory_dim+e_feat_dim+temporal_dim
self.updater = updater_dict[updater_type](input_size=self.message_dim,
hidden_size=memory_dim)
self.memory = memory
self.temporal_encoder = TimeEncode(temporal_dim)
# Here assume g is a subgraph from each iteration
def stick_feat_to_graph(self, g):
# How can I ensure order of the node ID
g.ndata['timestamp'] = self.memory.last_update_t[g.ndata[dgl.NID]]
g.ndata['memory'] = self.memory.memory[g.ndata[dgl.NID]]
def msg_fn_cat(self, edges):
src_delta_time = edges.data['timestamp'] - edges.src['timestamp']
time_encode = self.temporal_encoder(src_delta_time.unsqueeze(
dim=1)).view(len(edges.data['timestamp']), -1)
ret = torch.cat([edges.src['memory'], edges.dst['memory'],
edges.data['feats'], time_encode], dim=1)
return {'message': ret, 'timestamp': edges.data['timestamp']}
def agg_last(self, nodes):
timestamp, latest_idx = torch.max(nodes.mailbox['timestamp'], dim=1)
ret = nodes.mailbox['message'].gather(1, latest_idx.repeat(
self.message_dim).view(-1, 1, self.message_dim)).view(-1, self.message_dim)
return {'message_bar': ret.reshape(-1, self.message_dim), 'timestamp': timestamp}
def update_memory(self, nodes):
# It should pass the feature through RNN
ret = self.updater(
nodes.data['message_bar'].float(), nodes.data['memory'].float())
return {'memory': ret}
def forward(self, g):
self.stick_feat_to_graph(g)
g.update_all(self.msg_fn_cat, self.agg_last, self.update_memory)
return g
class TemporalGATConv(nn.Module):
"""Dot Product based embedding with temporal encoding
it will each node will compute the attention weight with other
nodes using other node's memory as well as edge features
Aggregation:
..math::
h_i^{(l+1)} = ReLu(\sum_{j\in \mathcal{N}(i)} \alpha_{i, j} (h_j^{(l)}(t)||e_{jj}||TimeEncode(t-t_j)))
\alpha_{i, j} = \mathrm{softmax_i}(\frac{QK^T}{\sqrt{d_k}})V
K,Q,V computation:
..math::
K = W_k[memory_{src}(t),memory_{dst}(t),TimeEncode(t_{dst}-t_{src})]
Q = W_q[memory_{src}(t),memory_{dst}(t),TimeEncode(t_{dst}-t_{src})]
V = W_v[memory_{src}(t),memory_{dst}(t),TimeEncode(t_{dst}-t_{src})]
Parameters
----------
edge_feats : int
dimension of edge feats
memory_feats : int
dimension of memory feats
temporal_feats : int
length of fourier series of time encoding
num_heads : int
number of head in multihead attention
allow_zero_in_degree : bool
Whether allow some node have indegree == 0 to prevent silence evaluation
Example
----------
>>> attn = TemporalGATConv(2,2,5,2,2,False)
>>> star_graph = dgl.graph(([1,2,3,4,5],[0,0,0,0,0]))
>>> star_graph.edata['feats'] = torch.ones(5,2).double()
>>> star_graph.edata['timestamp'] = torch.zeros(5).double()
>>> memory = torch.ones(6,2)
>>> ts = torch.random.rand(6).double()
>>> star_graph = dgl.add_self_loop(star_graph)
>>> attn(graph,memory,ts)
tensor([[-0.0924, -0.3842],
[-0.0840, -0.3539],
[-0.0842, -0.3543],
[-0.0838, -0.3536],
[-0.0856, -0.3568],
[-0.0858, -0.3572]], grad_fn=<AddmmBackward>)
"""
def __init__(self,
edge_feats,
memory_feats,
temporal_feats,
out_feats,
num_heads,
allow_zero_in_degree=False):
super(TemporalGATConv, self).__init__()
self._edge_feats = edge_feats
self._memory_feats = memory_feats
self._temporal_feats = temporal_feats
self._out_feats = out_feats
self._allow_zero_in_degree = allow_zero_in_degree
self._num_heads = num_heads
self.fc_Q = nn.Linear(self._memory_feats+self._temporal_feats,
self._out_feats*self._num_heads, bias=False)
self.fc_K = nn.Linear(self._memory_feats+self._edge_feats +
self._temporal_feats, self._out_feats*self._num_heads, bias=False)
self.fc_V = nn.Linear(self._memory_feats+self._edge_feats +
self._temporal_feats, self._out_feats*self._num_heads, bias=False)
self.merge = MergeLayer(
self._memory_feats, self._out_feats*self._num_heads, 512, self._out_feats)
self.temporal_encoder = TimeEncode(self._temporal_feats)
def weight_fn(self, edges):
t0 = torch.zeros_like(edges.dst['timestamp'])
q = torch.cat([edges.dst['s'],
self.temporal_encoder(t0.unsqueeze(dim=1)).view(len(t0), -1)], dim=1)
time_diff = edges.data['timestamp']-edges.src['timestamp']
time_encode = self.temporal_encoder(
time_diff.unsqueeze(dim=1)).view(len(t0), -1)
k = torch.cat(
[edges.src['s'], edges.data['feats'], time_encode], dim=1)
squeezed_k = self.fc_K(
k.float()).view(-1, self._num_heads, self._out_feats)
squeezed_q = self.fc_Q(
q.float()).view(-1, self._num_heads, self._out_feats)
ret = torch.sum(squeezed_q*squeezed_k, dim=2)
return {'a': ret, 'efeat': squeezed_k}
def msg_fn(self, edges):
ret = edges.data['sa'].view(-1, self._num_heads, 1)*edges.data['efeat']
return {'attn': ret}
def forward(self, graph, memory, ts):
graph = graph.local_var() # Using local scope for graph
if not self._allow_zero_in_degree:
if(graph.in_degrees() == 0).any():
raise DGLError('There are 0-in-degree nodes in the graph, '
'output for those nodes will be invalid. '
'This is harmful for some applications, '
'causing silent performance regression. '
'Adding self-loop on the input graph by '
'calling `g = dgl.add_self_loop(g)` will resolve '
'the issue. Setting ``allow_zero_in_degree`` '
'to be `True` when constructing this module will '
'suppress the check and let the code run.')
#print("Shape: ",memory.shape,ts.shape)
graph.srcdata.update({'s': memory, 'timestamp': ts})
graph.dstdata.update({'s': memory, 'timestamp': ts})
# Dot product Calculate the attentio weight
graph.apply_edges(self.weight_fn)
# Edge softmax
graph.edata['sa'] = edge_softmax(
graph, graph.edata['a'])/(self._out_feats**0.5)
# Update dst node Here msg_fn include edge feature
graph.update_all(self.msg_fn, fn.sum('attn', 'agg_u'))
rst = graph.dstdata['agg_u']
# Implement skip connection
rst = self.merge(rst.view(-1, self._num_heads *
self._out_feats), graph.dstdata['s'])
return rst