Temporal Graph Neural Network:EvolveGCN、MPNN-LSTM

在本文中,我们将列举介绍时空信息的两种动态图,同时我们将列举时间序列在GNN架构上的不同应用。通过网络流量预测,我们将利用时间信息来改进我们的结果并获得可靠的预测。


前言

动态图和时态GNN开启了各种新的应用,如交通和网络流量预测、运动分类、流行病学预测、链路预测、电力系统预测等。时间序列预测特别受这种图表的欢迎,因为我们可以使用历史数据来预测系统的未来行为。

在本文中,我们将重点讨论具有时间组件的图。它们可以分为两类:

  • 具有时间信息的静态图:该图的邻接节点和节点权重不发生变化,但是节点特征和节点标签随着时间发生变化。
  • 具有时间信息的动态图:该图的节点和边以及节点特征、标签都会发生变化。

在第一个实例中,图的拓扑结构是静止的。例如,一个国家内的城市网络是静止的,城市与城市之间的连接是固定不变的,为了预测交通流量,城市的节点特征会发生变化。
在第二个实例中,节点及其连接是动态的。例如,一个社交网络中,用户与用户之间的连接会随着时间不断的发生变化。动态图在生活中更加常见,同时也更加难以预测。

在后文中,我们将介绍这两种网络架构。


一、预测网站人流量

在本节中,我们将使用时态GNN预测维基百科文章的流量(作为一个带有时态信号的静态图的例子)。在流量预测问题上,当天及将来的流量与过去的流量往往高度相关,因此我们需要建立一个时间模型,使其能够包含有关过去流量的信息。

1、数据集介绍

在本节中,我们希望在一个带有时间信号的静态图形上预测网络流量。WikiMaths数据集由1068篇文章组成,用节点表示。一个节点特征对应过去一天的访问次数(默认为8个特征)。边被加权,权重表示从源页面到目标页面的链接数。我们想要预测2019年3月16日至2021年3月15日期间这些维基百科页面的每日用户访问量,这将产生731个子图。每个子图都是描述系统在特定时间的状态的图形。

该数据集是静态图,节点及其邻接节点是固定不变的,相连边的权重在通常情况下也固定不变。随着时间改变的量是1068篇节点的特征以及需要预测的每日用户访问量。

图1显示了用Gephi制作的WikiMaths的表示,其中节点的大小和颜色与它们的连接数成正比。
WikiMaths静态图

图1 WikiMaths静态图

2、EvolveGCN架构

对于这个任务,我们将使用EvolveGCN架构。以前的方法,如图卷积循环网络(GCN),使用带有图卷积算子的RNN来计算节点嵌入。相比之下,EvolveGCN将RNN应用于GCN参数本身。顾名思义,GCN随着时间的推移而发展,以产生相关的时间节点嵌入。图2展示了这个过程的高级视图。
EvolveGCN架构产生节点嵌入

图2 EvolveGCN架构产生节点嵌入

该架构有两个变体:

  • EvolveGCN-H:该变体考虑当前的节点嵌入以及过去的GCN参数。
  • EvolveGCN-O:该变体只考虑过去的GCN参数。

其中,EvolveGCN-H通常使用Gated Recurrent Unit(GRU)代替 Vanilla RNN。GRU是精简版本的长短期记忆(LSTM)单元,它可以达到令人满意的效果但是参数更少。它由一个重置门、一个更新门和一个单元状态组成。在此架构中,GRU在时刻 t t t更新第1层的GCN权值矩阵如下:
W t ( l ) = G R U ( H t ( l ) , W t − 1 ( l ) ) W_{t}^{\left( l \right)}=GRU\left( H_{t}^{\left( l \right)}, W_{t-1}^{\left( l \right)} \right) Wt(l)=GRU(Ht(l),Wt1(l))
式中, H t H_{t} Ht代表了 l l l 层在 t t t 时间的节点嵌入, W t − 1 W_{t-1} Wt1 l l l 层在前一个时间步长的权重矩阵。
输出的权重矩阵会被用于计算下一层的节点嵌入向量:
H t ( l + 1 ) = G C N ( A t , H t ( l ) , W t ( l ) ) = D ~ − 1 2 A ~ T D ~ − 1 2 X W T H_{t}^{\left( l+1 \right)}=GCN\left( A_t, H_{t}^{\left( l \right)}, W_{t}^{\left( l \right)} \right) \\ =\tilde{D}^{-\frac{1}{2}}\tilde{A}^T\tilde{D}^{-\frac{1}{2}}XW^T Ht(l+1)=GCN(At,Ht(l),Wt(l))=D~21A~TD~21XWT
式中, A ~ \tilde{A} A~是包括自循环的邻接矩阵。
上述步骤可以总结为:
EvolveGCN-H架构流程图

图3 EvolveGCN-H架构流程图

该架构中GRU的实现需要两种扩展:

  • 输入和隐藏层用矩阵代替向量去合理得存储GCN权重矩阵。
  • 输入的特征数需要同隐藏层的特征数相匹配,这意味着我们需要引入池化算子对节点向量进行汇总只保留适当的列数。

EvolveGCN-O变体不需要这些扩展。实际上,EvolveGCN-O是基于LSTM网络来模拟输入-输出关系的。我们不需要向LSTM提供隐藏状态,因为它已经包含了一个存储先前值的单元格。这种机制简化了更新步骤,可以这样写:
W t ( l ) = L S T M ( W t − 1 ( l ) ) W_{t}^{\left( l \right)}=LSTM\left( W_{t-1}^{\left( l \right)} \right) Wt(l)=LSTM(Wt1(l))

生成的GCN权重矩阵以相同的方式用于生成下一层的节点嵌入:
H t ( l + 1 ) = G C N ( A t , H t ( l ) , W t ( l ) ) = D ~ − 1 2 A ~ T D ~ − 1 2 X W T H_{t}^{\left( l+1 \right)}=GCN\left( A_t, H_{t}^{\left( l \right)}, W_{t}^{\left( l \right)} \right) \\ =\tilde{D}^{-\frac{1}{2}}\tilde{A}^T\tilde{D}^{-\frac{1}{2}}XW^T Ht(l+1)=GCN(At,Ht(l),Wt(l))=D~21A~TD~21XWT

该架构更加简单,因为其时间维度只依赖于LSTM网络。
上述步骤可以总结为。
EvolveGCN-O架构流程图

图4 EvolveGCN-O架构流程图

那么上述两种架构,我们应该使用哪一种呢?最好的解决办法是依赖于数据的特点:

  • EvolveGCN-H架构在节点特征是必要时表现更好,因为它能够显式地包含节点嵌入。
  • EvolveGCN-O架构在图结构发挥重要作用时表现地更好,因为它更多地关注拓扑结构地变化。

请注意,这些评论主要是理论性的,这就是为什么在应用程序中测试这两种变体会很有帮助。这就是我们通过实现这些网络流量预测模型所要做的。

3、应用EvolveGCN架构

3.1 数据库导入

import torch
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

from tqdm import tqdm
from torch_geometric_temporal.signal import temporal_signal_split
from torch_geometric_temporal.dataset import WikiMathsDatasetLoader
from torch_geometric_temporal.nn.recurrent import EvolveGCNH
from torch_geometric_temporal.nn.recurrent import EvolveGCNO

torch.manual_seed(42)
torch.cuda.manual_seed(42)
torch.cuda.manual_seed_all(42)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

3.2 初步观察数据集

# Input Data
# dataset[0] describes the graph at time step 0
dataset = WikiMathsDatasetLoader().get_dataset()
train_dataset, test_dataset = temporal_signal_split(dataset, train_ratio=0.5)

# Preliminary visualization
mean_cases = [snapshot.y.mean().item() for snapshot in dataset]
std_cases = [snapshot.y.std().item() for snapshot in dataset]
df = pd.DataFrame(mean_cases, columns=['mean'])
df['std'] = pd.DataFrame(std_cases, columns=['std'])
df['rolling'] = df['mean'].rolling(7).mean()

plt.figure(figsize=(10, 5))
plt.plot(df['mean'], 'k-', label='Mean')
plt.plot(df['rolling'], 'g-', label='Moving average')
plt.grid(linestyle=':')
plt.fill_between(df.index, df['mean'] - df['std'], df['mean'] + df['std'], color='r', alpha=0.1)
plt.axvline(x=360, color='b', linestyle='--')
plt.text(360, 1.5, 'Train/test split', rotation=-90, color='b')
plt.xlabel('Time (days)')
plt.ylabel('Normalized number of visits')
plt.legend(loc='upper right')
plt.show()

在这里插入图片描述

图5 WikiMaths数据集

从图5中可以发现,WikiMaths所需要预测的值已经经过了归一化处理,范围在[-1, 1]之间。

3.3 EvolveGCN-H类

class TemporalGNN(torch.nn.Module):
    def __init__(self, node_count, dim_in):
        super().__init__()
        self.recurrent = EvolveGCNH(node_count, dim_in)
        self.linear = torch.nn.Linear(dim_in, 1)

    def forward(self, x, edge_index, edge_weight):
        h = self.recurrent(x, edge_index, edge_weight).relu()
        h = self.linear(h)
        return h

model = TemporalGNN(dataset[0].x.shape[0], dataset[0].x.shape[1])
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
model.train()

在上述代码中,EvolveGCN-H有两个输入,分别时节点数目和节点特征数。同时EvolveGCN构建了两层网络,分别是EvolveGCNH层和线性层,EvolveGCN层用于节点嵌入,线性层用于输出预测结果。

EvolveGCN函数中的实现流程如下。
首先,利用池化算子对节点特征向量进行归纳。

X_tilde = self.pooling_layer(X, edge_index)
# perm = topk(score, self.ratio, batch, self.min_score)

该算子将按序返回得分最高的池化向量。
之后利用GRU更新权重。

X_tilde, W = self.recurrent_layer(X_tilde, W)

最后,利用GCN输出节点嵌入

X = self.conv_layer(W.squeeze(dim=0), X, edge_index, edge_weight)

3.4 训练EvolveGCN-H模型

# Training
for epoch in tqdm(range(50)):
    for i, snapshot in enumerate(train_dataset):
        y_pred = model(snapshot.x, snapshot.edge_index, snapshot.edge_attr)
        loss = torch.mean((y_pred-snapshot.y)**2)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

# Evaluation
model.eval()
loss = 0
for i, snapshot in enumerate(test_dataset):
    y_pred = model(snapshot.x, snapshot.edge_index, snapshot.edge_attr)
    mse = torch.mean((y_pred-snapshot.y)**2)
    loss += mse
loss = loss / (i+1)
print(f'MSE = {loss.item():.4f}')

MSE = 0.7588
可视化模型预测的结果。

# visual results
y_preds = [model(snapshot.x, snapshot.edge_index, snapshot.edge_attr).squeeze().detach().numpy().mean() for snapshot in test_dataset]

plt.figure(figsize=(10,5), dpi=300)
plt.plot(df['mean'], 'k-', label='Mean')
plt.plot(df['rolling'], 'g-', label='Moving average')
plt.plot(range(360,722), y_preds, 'r-', label='Prediction')
plt.grid(linestyle=':')
plt.fill_between(df.index, df['mean']-df['std'], df['mean']+df['std'], color='r', alpha=0.1)
plt.axvline(x=360, color='b', linestyle='--')
plt.text(360, 1.5, 'Train/test split', rotation=-90, color='b')
plt.xlabel('Time (days)')
plt.ylabel('Normalized number of visits')
plt.legend(loc='upper right')
plt.show()

y_pred = model(test_dataset[0].x, test_dataset[0].edge_index, test_dataset[0].edge_attr).detach().squeeze().numpy()

plt.figure(figsize=(10,5), dpi=300)
sns.regplot(x=test_dataset[0].y.numpy(), y=y_pred)
plt.show()
图6 EvolveGCN-H结果可视化

从图6中我们观察到预测值和实测值之间存在适度的正相关。我们的模型不是非常准确,但前面的图表明它很好地理解了数据的周期性。

3.5 EvolveGCN-O类

class TemporalGNN(torch.nn.Module):
    def __init__(self, dim_in):
        super().__init__()
        # without pooling_layer to feature extraction
        self.recurrent = EvolveGCNO(dim_in, 1)
        self.linear = torch.nn.Linear(dim_in, 1)

    def forward(self, x, edge_index, edge_weight):
        h = self.recurrent(x, edge_index, edge_weight).relu()
        h = self.linear(h)
        return h

modelo = TemporalGNN(dataset[0].x.shape[1])
optimizero = torch.optim.Adam(modelo.parameters(), lr=0.01)
modelo.train()

EvolveGCNO函数同EvolveGCNH函数类似,唯一的区别在于EvolveGCNO函数中没有池化层对输入特征进行归纳。在此不多赘述。

3.6 训练EvolveGCN-O模型

# Training
for epoch in tqdm(range(50)):
    for i, snapshot in enumerate(train_dataset):
        y_pred = modelo(snapshot.x, snapshot.edge_index, snapshot.edge_attr)
        loss = torch.mean((y_pred-snapshot.y)**2)
        loss.backward()
        optimizero.step()
        optimizero.zero_grad()

# Evaluation
modelo.eval()
loss = 0
for i, snapshot in enumerate(test_dataset):
    y_pred = modelo(snapshot.x, snapshot.edge_index, snapshot.edge_attr)
    mse = torch.mean((y_pred-snapshot.y)**2)
    loss += mse
loss = loss / (i+1)
print(f'MSE = {loss.item():.4f}')

MSE = 0.7338
平均而言,EvolveGCN-O模型得到了相似的结果,平均MSE为0.7338。在这种情况下,使用GRU或LSTM网络不会影响预测。这是可以理解的,因为节点特征(EvolveGCN-H)中包含的过去访问次数和页面之间的连接(EvolveGCN-O)都是必不可少的。因此,这种GNN架构特别适合这种流量预测任务。

二、COVID-19病例预测

第一个实例是时间序列的GNN模型在静态图上的应用,接下来我们将介绍GNN如何运用在动态图中。

1、数据集介绍

本节将重点介绍流行病预报的新应用。我们将使用英格兰Covid动态数据集。虽然节点是静态的,但连接和边的权重会随时间变化。该数据集代表了2020年3月3日至5月12日期间129个英格兰NUTS 3地区报告的COVID-19病例数。数据是从安装了Facebook应用程序并分享其位置历史的手机上收集的。我们的目标是预测1天内每个节点(地区)的病例数。

该数据集由47个子图组成,节点的特征向量为节点所代表的地区前 d d d 天的病例数,边是无方向但有权重,权重代表两个节点地区的流动人数。最后,子图也包括了自循环人口流动。

2、MPNN-LSTM架构

MPNN-LSTM融合了MPNN和LSTM网络。该架构将具有相应边索引和权值的输入节点特征馈送到GCN层。同时对GCN层的输出运用批处理归一化和dropout。这一过程将被重复第二次用于生成节点嵌入矩阵 H ( t ) H^{\left( t \right)} H(t)。我们将不同的时间步运用MPNN来创建一个序列矩阵 H ( 1 ) H^{\left( 1 \right)} H(1),…, H ( t ) H^{\left( t \right)} H(t)。该序列矩阵将被输送到第二层LSTM网络中去捕捉子图中的时间信息。最后,我们应用线性变换网络和ReLU激活函数去输出预测值。

总体的流程图如下。
MPNN-LSTM架构

图7 MPNN-LSTM架构

MPNN-LSTM的作者指出,它不是英格兰Covid数据集上表现最好的模型(具有两级GNN的MPNN是)。然而,这是一种有趣的方法,可以在其他场景中执行得更好。他们还指出,它更适合于长期预测,比如未来14天的预测,而不是像我们这个数据集的版本中那样的一天。尽管存在这个问题,我们还是为了方便而使用后者,因为它不会影响解决方案的设计。

3、运用MPNN-LSTM架构

3.1 数据库导入

import torch
import pandas as pd
import matplotlib.pyplot as plt
from tqdm import tqdm

from torch_geometric_temporal.dataset import EnglandCovidDatasetLoader
from torch_geometric_temporal.signal import temporal_signal_split
from torch_geometric_temporal.nn.recurrent import MPNNLSTM

torch.manual_seed(42)
torch.cuda.manual_seed(42)
torch.cuda.manual_seed_all(42)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

3.2 初步观察数据集

ataset = EnglandCovidDatasetLoader().get_dataset(lags=14)
train_dataset, test_dataset = temporal_signal_split(dataset, train_ratio=0.8)

mean_cases = [snapshot.y.mean().item() for snapshot in dataset]
std_cases = [snapshot.y.std().item() for snapshot in dataset]
df = pd.DataFrame(mean_cases, columns=['mean'])
df['std'] = pd.DataFrame(std_cases, columns=['std'])

plt.figure(figsize=(10, 5))
plt.plot(df['mean'], 'k-')
plt.grid(linestyle=':')
plt.fill_between(df.index, df['mean'] - df['std'], df['mean'] + df['std'], color='r', alpha=0.1)
plt.axvline(x=38, color='b', linestyle='--', label='Train/test split')
plt.text(38, 1, 'Train/test split', rotation=-90, color='b')
plt.xlabel('Reports')
plt.ylabel('Mean normalized number of cases')
plt.show()

在这里插入图片描述

图8 英格兰病例数数据集

该图显示了大量的波动和低数量的子图。这就是为什么我们在这个例子中使用80/20训练测试分割。然而,在如此小的数据集上获得良好的性能可能具有挑战性。

3.3 MPNN-LSTM类

class TemporalGNN(torch.nn.Module):
    def __init__(self, dim_in, dim_h, num_nodes):
        super().__init__()
        self.recurrent = MPNNLSTM(dim_in, dim_h, num_nodes, 1, 0.5)
        self.dropout = torch.nn.Dropout(0.5)
        self.linear = torch.nn.Linear(2*dim_h + dim_in, 1)

    def forward(self, x, edge_index, edge_weight):
        h = self.recurrent(x, edge_index, edge_weight).relu()
        h = self.dropout(h)
        h = self.linear(h).tanh()
        return h

model = TemporalGNN(dataset[0].x.shape[1], 64, dataset[0].x.shape[0])
print(model)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
model.train()

上述代码中最关键的是MPNNLSTM函数,让我们来解析一下该函数的内部运行流程。

首先,输入的节点信息将依次两个相同结构的GCN层,至于构建多个GCN层的优点可以查看GIN

        X = self._graph_convolution_1(X, edge_index, edge_weight)
        R.append(X)
        X = self._graph_convolution_2(X, edge_index, edge_weight)
        R.append(X)
        X = torch.cat(R, dim=1)

_ g r a p h _ c o n v o l u t i o n _ x \_graph\_convolution\_x _graph_convolution_x内部,主要将节点信息经过GCN层、激活函数、归一化以及dropout层。

    def _graph_convolution_1(self, X, edge_index, edge_weight):
    	# self._convolution_1 = GCNConv(self.in_channels, self.hidden_size)
        X = F.relu(self._convolution_1(X, edge_index, edge_weight))
        X = self._batch_norm_1(X)
        X = F.dropout(X, p=self.dropout, training=self.training)
        return X

上述内容主要完成了MPNN架构部分,之后是LSTM架构的处理。

		# self._recurrent_1 = nn.LSTM(2 * self.hidden_size, self.hidden_size, 1)
		# self._recurrent_2 = nn.LSTM(self.hidden_size, self.hidden_size, 1)
        X, (H_1, _) = self._recurrent_1(X)
        X, (H_2, _) = self._recurrent_2(X)

该架构的主要目的是为了处理嵌入向量中的时间信息。
最后返回的是LSTM架构输出的嵌入向量和初始节点特征 S S S

        H = torch.cat([H_1[0, :, :], H_2[0, :, :], S], dim=1)
        return H

至此,MPNN-LSTM架构建立完毕。

3.4 训练MPNN-LSTM模型

# Training
for epoch in tqdm(range(100)):
    loss = 0
    for i, snapshot in enumerate(train_dataset):
        y_pred = model(snapshot.x, snapshot.edge_index, snapshot.edge_attr)
        loss = loss + torch.mean((y_pred-snapshot.y)**2)
    loss = loss / (i+1)
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

# Evaluation
model.eval()
loss = 0
for i, snapshot in enumerate(test_dataset):
    y_pred = model(snapshot.x, snapshot.edge_index, snapshot.edge_attr)
    mse = torch.mean((y_pred-snapshot.y)**2)
    loss += mse
loss = loss / (i+1)
print(f'MSE: {loss.item():.4f}')

MSE: 1.3820
绝对误差似乎有些大,让我们来看一下预测的结果。

图9 MPNN-LSTM预测结果图

正如预期的那样,预测值与实际情况不太相符。这可能是由于缺乏数据(子图一共仅有47个):我们的模型学习了最小化MSE损失的平均值,但无法拟合曲线并理解其周期性。同时散点图显示相关性很弱。我们看到,预测(y轴)主要围绕0.35,方差很小。这与从-1.5到0.6的基本真值不对应。根据我们的实验,添加第二个线性层并没有改善MPNN-LSTM的预测。可以实施一些策略来帮助模型。
首先,更多的数据点会有很大帮助,因为这是一个小数据集。此外,时间序列包含两个有趣的特征:趋势(随时间持续增加和减少)和季节性(可预测的模式)。我们可以添加一个预处理步骤来去除这些特征,这些特征会给我们想要预测的信号增加噪声。除了循环神经网络之外,自关注GIN是另一种创建时间GNN的流行技术。注意力可以局限于时间信息,也可以考虑空间数据,通常由图卷积处理。最后,时间GNN也可以扩展到前一章中描述的异构设置heterogeneous GNNs。不幸的是,这种结合需要更多的数据,目前是一个活跃的研究领域。


总结

本文介绍了一种具有时空信息的新型图。这个时间分量在许多应用中都很有用,主要与时间序列预测有关。我们描述了适合这种描述的两种类型的图:静态图,其中特征随着时间的推移而变化,以及动态图,其中特征和拓扑可以改变。此外,我们还介绍了时态GNN的两种应用。首先,我们实现了EvolveGCN架构,它使用GRU或LSTM网络来更新GCN参数。我们通过重新访问网络流量预测来应用它,并在有限的数据集上取得了出色的结果。其次,采用MPNN-LSTM结构进行疫情预测。我们对英格兰Covid数据集应用了一个带有时间信号的动态图,但它的小尺寸使我们无法获得可靠的结果。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值