摘要
本周阅读了一篇用于多变量时间序列预测的多尺度自适应图神经网络的文章,多变量时间序列(MTS)预测在智能应用的自动化和最优化中发挥着重要作用。这是一项具有挑战性的任务。本文提出了一种多尺度自适应图神经网络(MAGNN)来解决上述问题。MAGNN利用多尺度金字塔网络在不同的时间尺度上保持潜在的时间依赖关系。作者还开发了一个基于尺度的融合模块,有效地促进了不同时间尺度上的协作,并自动捕捉贡献的时间模式的重要性。在六个真实数据集上的实验表明,MAGNN在各种设置下的性能都优于最先进的方法。
Abstract
This week,an article on multi-scale adaptive graph neural network for multivariable time series prediction is readed. Multivariable time series (MTS) prediction plays an important role in the automation and optimization of intelligent applications. This is a challenging task. In this paper, a multi-scale adaptive graph neural network (MAGNN) is proposed to solve the above problems. MAGNN uses multi-scale pyramid network to maintain potential time dependence on different time scales. The author also developed a scale-based fusion module, which effectively promoted the cooperation on different time scales and automatically captured the importance of the contribution time pattern. Experiments on six real data sets show that the performance of MAGNN is better than the most advanced methods in various settings.
文献阅读
题目
Multi-Scale Adaptive Graph Neural Network for Multivariate Time Series Forecasting
问题
1)传统的方法,如向量自回归(VAR)、时间正则化矩阵分解(TRMF)、向量自回归滑动平均(VARMA)和高斯过程(GP),往往依赖于严格的平稳假设,无法捕捉变量之间的非线性相关性。
2)注意机制和基于记忆的网络模型侧重于时间相关性的建模,将MTS的输入处理为向量,并假设单个变量的预测值受所有其他变量的影响,这在实际应用中是不合理的,也是很难满足的。例如,一条街道的TrafficflOWS很大程度上受到邻近街道的影响,而来自远处街道的影响相对较小。
3)现有的GNN模型只考虑了单个时间尺度上的时间依赖关系,这可能不能正确地反映许多现实世界场景中的变化。已有的工作学习共享邻接矩阵来表示丰富的变量间依赖关系,这使得模型偏向于学习一种突出的共享时态模式。
本文贡献
1)提出MAGNN,它学习一种既能综合反映多尺度时态模式又能反映特定尺度变量间依赖关系的时态表示。
2)设计了一个自适应图学习模块,用于探索不同时间尺度下丰富和隐含的变量间依赖关系,以及一个基于尺度的融合模块,用于促进这些特定尺度的时间表示之间的协作,并自动捕获贡献的时间模式的重要性。
3)在六个真实世界的MTS基准数据集上进行广泛的实验。实验结果表明,该方法的性能优于目前最先进的方法。
问题描述
本文主要研究MTS预测。在形式上,给定一个输入的时间序列数据,其中表示时间步t的值,N是变量维度,表示第t个时间步的第i个变量的值,MTS预测的目的是预测未来在时间步t+h的值,其中h表示需要预测的未来时间步数。这个问题可以表述为:。式中F为映射函数,θ表示所有可学习的参数。
然后,存在如下几种关于MTS预测的定义:
定义1:MTS数据用图结构表示。图被定义为G=(V,E),其中V表示节点集,|V|=N,E是边集。假设,将第i个变量视为第i个节点,的值是的特征,每条边的都表明vi和vj之间存在变量间的依赖关系。
定义2:加权邻接矩阵。图的加权邻接矩阵是一种用于存储边权重的数学表示方法,其中如果,;如果,。对于没有任何先验知识的纯MTS数据,需要学习多图的加权邻接矩阵来表示丰富且隐式的变量间依赖关系。据此,MTS预测公式可修改为:。其中表示能被GNN用于MTS预测的图集。
图神经网络
论文方法中应用的图卷积操作定义如下:
其中G=(V,E,A)是一个带加权邻接矩阵的图,x是节点的表示,σ是一个激活函数,θ是可学习的参数矩阵,是具有自连接的邻接矩阵,是的对角度矩阵,。通过将图卷积操作多层堆叠,可以聚合多阶的邻居信息。
多尺度GNN,又称分层GNN,通常在细粒度图的基础上分层构建粗粒度图。MAGNN关注时间维度的尺度,与一般的多尺度GNN非常不同,后者主要关注空间维度的尺度。MAGNN引入了一个多尺度金字塔网络,将原始时间序列转换为从较小尺度到较大尺度的特征表示,在该网络上,它学习每个尺度下具有相同大小的特定尺度图,并对每个图使用公式定义的基本GNN。
Framework
下图说明了MAGNN的框架,它包括四个主要部分:a)多尺度金字塔网络,用于在不同的时间尺度上保持底层的时间层次;b)自适应图学习模块,用于自动推断变量间的依赖关系;c)多尺度时间图神经网络,用于捕获各种尺度特定的时间模式;d)尺度级融合模块,用于有效地促进跨不同时间尺度的协作。
MAGNN框架的4个主要部分组成具体作用如下:(a)两个并行的卷积神经网络和每层的逐点相加将特征表示从较小尺度分层变换到较大尺度;(b)自适应图学习模块将节点嵌入和尺度嵌入作为输入,并输出特定尺度的邻接矩阵;©将每个尺度特定的特征表示和邻接矩阵输入到时序图神经网络(TGNN)中,以获得尺度特定的表示。(d)加权融合特定尺度表示以捕获贡献的时间模式。最终的多尺度表示被送入包括两个卷积神经网络的输出模块以获得预测值。
实验
数据集
为了评估MAGNN的性能,在六个公共基准数据集上进行了实验:太阳能、交通、电力、汇率、纳斯达克和METR-LA。表一汇总了数据集的统计数据,6个公共基准数据集的详细情况如下:
太阳能:此数据集包含从国家可再生能源实验室收集的太阳能,2007年从阿拉巴马州的137个光伏发电厂每10分钟采样一次。
交通:此数据集包含加州交通部的道路使用率(在0到1之间),这些数据是从2015年到2016年旧金山湾区的862个传感器每小时汇总的。
用电量:此数据集包含UCI机器学习存储库的用电量,从2012年到2014年每小时汇总321个客户端的用电量。R汇率:该数据集包含八个国家的汇率,从1990年到2016年每天都进行抽样。
Nasdaq:这个数据集包含82家公司的股票价格,从2016年7月到2016年12月每分钟抽样一次。
METR-LA:此数据集包含洛杉矶县的平均车速,这些车速是从2012年3月至2012年6月高速公路上的207个环路探测器收集的5分钟数据。
将六个数据集按时间顺序分为训练集(60%)、验证集(20%)和测试集(20%)。
输入窗口大小T设置为168。学习率设置为0.001。使用ADAM优化器,所有可训练的参数都可以通过反向传播进行优化。对于所有数据集,尺度数为4。多尺度金字塔网络中的CNN的核大小从金字塔网络的第一层到最后一层分别设置为1×7、1×6和1×3,所有CNN的步长设置为2。作者分别设置地平线h={3,6,12,24},对于Nasdaq数据集,预测范围被设置为3到24分钟,对于METR-LA数据集,从15到120分钟,对于太阳能数据集,从30到240分钟,对于交通和电力数据集,从3到24小时,对于汇率数据集,从3到24天。预测范围越大,预测就越难。
实验结果
六个数据集上所有方法的结果总结如下表(按RSE表示):
在六个真实数据集上的实验表明,MAGNN在各种设置下的性能都优于最先进的方法。
深度学习
MAGNN模型相关代码
from layer import *
# from AGCRN import *
import torch
class magnn(nn.Module):
def __init__(self, gcn_depth, num_nodes, device, dropout=0.3, subgraph_size=20, node_dim=40, conv_channels=32, gnn_channels=32, scale_channels=16, end_channels=128, seq_length=12, in_dim=1, out_dim=12, layers=3, propalpha=0.05, tanhalpha=3, single_step=True):
super(magnn, self).__init__()
self.num_nodes = num_nodes
self.dropout = dropout
self.device = device
self.single_step = single_step
self.filter_convs = nn.ModuleList()
self.gate_convs = nn.ModuleList()
self.scale_convs = nn.ModuleList()
self.gconv1 = nn.ModuleList()
self.gconv2 = nn.ModuleList()
self.norm = nn.ModuleList()
self.seq_length = seq_length
self.layer_num = layers
self.gc = graph_constructor(num_nodes, subgraph_size, node_dim, self.layer_num, device)
if self.single_step:
self.kernel_set = [7, 6, 3, 2]
else:
self.kernel_set = [3, 2, 2]
self.scale_id = torch.autograd.Variable(torch.randn(self.layer_num, device=self.device), requires_grad=True)
# self.scale_id = torch.arange(self.layer_num).to(device)
self.lin1 = nn.Linear(self.layer_num, self.layer_num)
self.idx = torch.arange(self.num_nodes).to(device)
self.scale_idx = torch.arange(self.num_nodes).to(device)
self.scale0 = nn.Conv2d(in_channels=in_dim, out_channels=scale_channels, kernel_size=(1, self.seq_length), bias=True)
self.multi_scale_block = multi_scale_block(in_dim, conv_channels, self.num_nodes, self.seq_length, self.layer_num, self.kernel_set)
# self.agcrn = nn.ModuleList()
length_set = []
length_set.append(self.seq_length-self.kernel_set[0]+1)
for i in range(1, self.layer_num):
length_set.append( int( (length_set[i-1]-self.kernel_set[i])/2 ) )
for i in range(self.layer_num):
"""
RNN based model
"""
# self.agcrn.append(AGCRN(num_nodes=self.num_nodes, input_dim=conv_channels, hidden_dim=scale_channels, num_layers=1) )
self.gconv1.append(mixprop(conv_channels, gnn_channels, gcn_depth, dropout, propalpha))
self.gconv2.append(mixprop(conv_channels, gnn_channels, gcn_depth, dropout, propalpha))
self.scale_convs.append(nn.Conv2d(in_channels=conv_channels,
out_channels=scale_channels,
kernel_size=(1, length_set[i])))
self.gated_fusion = gated_fusion(scale_channels, self.layer_num)
# self.output = linear(self.layer_num*self.hidden_dim, out_dim)
self.end_conv_1 = nn.Conv2d(in_channels=scale_channels,
out_channels=end_channels,
kernel_size=(1,1),
bias=True)
self.end_conv_2 = nn.Conv2d(in_channels=end_channels,
out_channels=out_dim,
kernel_size=(1,1),
bias=True)
def forward(self, input, idx=None):
seq_len = input.size(3)
assert seq_len==self.seq_length, 'input sequence length not equal to preset sequence length'
scale = self.multi_scale_block(input, self.idx)
# self.scale_weight = self.lin1(self.scale_id)
self.scale_set = [1, 0.8, 0.6, 0.5]
adj_matrix = self.gc(self.idx, self.scale_idx, self.scale_set)
outputs = self.scale0(F.dropout(input, self.dropout, training=self.training))
out = []
out.append(outputs)
for i in range(self.layer_num):
"""
RNN-based model
"""
# output = self.agcrn[i](scale[i].permute(0, 3, 2, 1), adj_matrix) # B T N D
# output = output.permute(0, 3, 2, 1)
output = self.gconv1[i](scale[i], adj_matrix[i])+self.gconv2[i](scale[i], adj_matrix[i].transpose(1,0))
scale_specific_output = self.scale_convs[i](output)
out.append(scale_specific_output)
# concatenate
# outputs = outputs + scale_specific_output
# mean-pooling
# outputs = torch.mean(torch.stack(out), dim=0)
out0 = torch.cat(out, dim=1)
out1 = torch.stack(out, dim = 1)
if self.single_step:
outputs = self.gated_fusion(out0, out1)
x = F.relu(outputs)
x = F.relu(self.end_conv_1(x))
x = self.end_conv_2(x)
return x, adj_matrix
网络层定义如下:
from __future__ import division
import torch
import torch.nn as nn
from torch.nn import init
import numbers
import torch.nn.functional as F
import numpy as np
class nconv(nn.Module):
def __init__(self):
super(nconv,self).__init__()
def forward(self,x, A):
x = torch.einsum('ncvl,vw->ncwl',(x,A))
return x.contiguous()
class dy_nconv(nn.Module):
def __init__(self):
super(dy_nconv,self).__init__()
def forward(self,x, A):
x = torch.einsum('ncvl,nvwl->ncwl',(x,A))
return x.contiguous()
class linear(nn.Module):
def __init__(self,c_in,c_out,bias=True):
super(linear,self).__init__()
self.mlp = torch.nn.Conv2d(c_in, c_out, kernel_size=(1, 1), padding=(0,0), stride=(1,1), bias=bias)
def forward(self,x):
return self.mlp(x)
class layer_block(nn.Module):
def __init__(self, c_in, c_out, k_size):
super(layer_block, self).__init__()
self.conv_output = nn.Conv2d(c_in, c_out, kernel_size=(1, 1), stride=(1, 2))
self.conv_output1 = nn.Conv2d(c_in, c_out, kernel_size=(1, k_size), stride=(1, 1), padding=(0, int( (k_size-1)/2 ) ) )
self.output = nn.MaxPool2d(kernel_size=(1,3), stride=(1,2), padding=(0,1))
self.conv_output1 = nn.Conv2d(c_in, c_out, kernel_size=(1, k_size), stride=(1, 1) )
self.output = nn.MaxPool2d(kernel_size=(1,3), stride=(1,2))
self.relu = nn.ReLU()
def forward(self, input):
conv_output = self.conv_output(input) # shape (B, D, N, T)
conv_output1 = self.conv_output1(input)
output = self.output(conv_output1)
return self.relu( output+conv_output[...,-output.shape[3]:] )
# return self.relu( conv_output )
class multi_scale_block(nn.Module):
def __init__(self, c_in, c_out, num_nodes, seq_length, layer_num, kernel_set, layer_norm_affline=True):
super(multi_scale_block, self).__init__()
self.seq_length = seq_length
self.layer_num = layer_num
self.norm = nn.ModuleList()
self.scale = nn.ModuleList()
for i in range(self.layer_num):
self.norm.append(nn.BatchNorm2d(c_out, affine=False))
# # self.norm.append(LayerNorm((c_out, num_nodes, int(self.seq_length/2**i)),elementwise_affine=layer_norm_affline))
# self.norm.append(LayerNorm((c_out, num_nodes, length_set[i]),elementwise_affine=layer_norm_affline))
self.start_conv = nn.Conv2d(c_in, c_out, kernel_size=(1, 1), stride=(1, 1))
self.scale.append(nn.Conv2d(c_out, c_out, kernel_size=(1, kernel_set[0]), stride=(1, 1)))
for i in range(1, self.layer_num):
self.scale.append(layer_block(c_out, c_out, kernel_set[i]))
def forward(self, input, idx): # input shape: B D N T
self.idx = idx
scale = []
scale_temp = input
scale_temp = self.start_conv(scale_temp)
# scale.append(scale_temp)
for i in range(self.layer_num):
scale_temp = self.scale[i](scale_temp)
# scale_temp = self.norm[i](scale_temp)
# scale_temp = self.norm[i](scale_temp, self.idx)
# scale.append(scale_temp[...,-self.k:])
scale.append(scale_temp)
return scale
class top_down_path(nn.Module):
def __init__(self, c_in, c_out_1, c_out_2, c_out_3, c_out_4):
super(top_down_path, self).__init__()
self.down1 = nn.Conv2d(c_in, c_out_1, kernel_size=(1, 1), stride=(1, 1))
self.down2 = nn.Conv2d(c_out_1, c_out_2, kernel_size=(1, 7), stride=(1, 2), padding=(0, 2))
self.down3 = nn.Conv2d(c_out_2, c_out_3, kernel_size=(1, 6), stride=(1, 2))
self.down4 = nn.Conv2d(c_out_3, c_out_4, kernel_size=(1, 3), stride=(1, 2))
self.up3 = nn.ConvTranspose2d(c_out_4, c_out_3, kernel_size=(1,3), stride=(1,2))
# self.up3 = nn.MaxUnpool2d()
self.up2 = nn.ConvTranspose2d(c_out_3, c_out_2, kernel_size=(1,6), stride=(1,2), output_padding=(0,1))
self.up1 = nn.ConvTranspose2d(c_out_2, c_out_1, kernel_size=(1,7), stride=(1,2))
def forward(self, input):
down_1 = self.down1(input)
down_2 = self.down2(down_1)
down_3 = self.down3(down_2)
down_4 = self.down4(down_3)
up_3 = self.up3(down_4)
output_3 = down_3 + up_3
up_2 = self.up2(output_3)
output_2 = down_2 + up_2
up_1 = self.up3(output_2)
output_1 = down_1[:,:,:,1:] + up_1
return down_4, output_3, output_2, output_1
class gated_fusion(nn.Module):
def __init__(self, skip_channels, layer_num, ratio=1):
super(gated_fusion, self).__init__()
# self.reduce = torch.mean(x,dim=2,keepdim=True)
self.dense1 = nn.Linear(in_features=skip_channels*(layer_num+1), out_features=(layer_num+1)*ratio, bias=False)
self.dense2 = nn.Linear(in_features=(layer_num+1)*ratio, out_features=(layer_num+1), bias=False)
def forward(self, input1, input2):
se = torch.mean(input1, dim=2, keepdim=False)
se = torch.squeeze(se)
se = F.relu(self.dense1(se))
se = F.sigmoid(self.dense2(se))
se = torch.unsqueeze(se, -1)
se = torch.unsqueeze(se, -1)
se = torch.unsqueeze(se, -1)
x = torch.mul(input2, se)
x = torch.mean(x, dim=1, keepdim=False)
return x
class prop(nn.Module):
def __init__(self,c_in,c_out,gdep,dropout,alpha):
super(prop, self).__init__()
self.nconv = nconv()
self.mlp = linear(c_in,c_out)
self.gdep = gdep
self.dropout = dropout
self.alpha = alpha
def forward(self,x,adj):
adj = adj + torch.eye(adj.size(0)).to(x.device)
d = adj.sum(1)
h = x
dv = d
a = adj / dv.view(-1, 1)
for i in range(self.gdep):
h = self.alpha*x + (1-self.alpha)*self.nconv(h,a)
ho = self.mlp(h)
return ho
class mixprop(nn.Module):
def __init__(self,c_in,c_out,gdep,dropout,alpha):
super(mixprop, self).__init__()
self.nconv = nconv()
self.mlp = linear((gdep+1)*c_in,c_out)
self.gdep = gdep
self.dropout = dropout
self.alpha = alpha
def forward(self,x,adj):
adj = adj + torch.eye(adj.size(0)).to(x.device)
d = adj.sum(1)
h = x
out = [h]
a = adj / d.view(-1, 1)
for i in range(self.gdep):
h = self.alpha*x + (1-self.alpha)*self.nconv(h,a)
out.append(h)
ho = torch.cat(out,dim=1)
ho = self.mlp(ho)
return ho
class graph_constructor(nn.Module):
def __init__(self, nnodes, k, dim, layer_num, device, alpha=3):
super(graph_constructor, self).__init__()
self.nnodes = nnodes
self.layers = layer_num
self.emb1 = nn.Embedding(nnodes, dim)
self.emb2 = nn.Embedding(nnodes, dim)
self.lin1 = nn.ModuleList()
self.lin2 = nn.ModuleList()
for i in range(layer_num):
self.lin1.append(nn.Linear(dim,dim))
self.lin2.append(nn.Linear(dim,dim))
self.device = device
self.k = k
self.dim = dim
self.alpha = alpha
def forward(self, idx, scale_idx, scale_set):
nodevec1 = self.emb1(idx)
nodevec2 = self.emb2(idx)
adj_set = []
for i in range(self.layers):
nodevec1 = torch.tanh(self.alpha*self.lin1[i](nodevec1*scale_set[i]))
nodevec2 = torch.tanh(self.alpha*self.lin2[i](nodevec2*scale_set[i]))
a = torch.mm(nodevec1, nodevec2.transpose(1,0))-torch.mm(nodevec2, nodevec1.transpose(1,0))
adj0 = F.relu(torch.tanh(self.alpha*a))
mask = torch.zeros(idx.size(0), idx.size(0)).to(self.device)
mask.fill_(float('0'))
s1,t1 = adj0.topk(self.k,1)
mask.scatter_(1,t1,s1.fill_(1))
# print(mask)
adj = adj0*mask
adj_set.append(adj)
return adj_set
class graph_constructor_full(nn.Module):
def __init__(self, nnodes, k, dim, layer_num, device, alpha=3):
super(graph_constructor_full, self).__init__()
self.nnodes = nnodes
self.layers = layer_num
self.emb1 = nn.Embedding(nnodes, dim)
self.emb2 = nn.Embedding(nnodes, dim)
self.lin1 = nn.ModuleList()
self.lin2 = nn.ModuleList()
for i in range(self.layers):
self.lin1.append(nn.Linear(dim,dim))
self.lin2.append(nn.Linear(dim,dim))
self.device = device
self.k = k
self.dim = dim
self.alpha = alpha
self.static_feat = static_feat
def forward(self, idx, scale_idx, scale_set):
nodevec1 = self.emb1(idx)
nodevec2 = self.emb2(idx)
adj_set = []
for i in range(self.layers):
nodevec1 = torch.tanh(self.alpha*self.lin1[i](nodevec1*scale_set[i]))
nodevec2 = torch.tanh(self.alpha*self.lin2[i](nodevec2*scale_set[i]))
a = torch.mm(nodevec1, nodevec2.transpose(1,0))-torch.mm(nodevec2, nodevec1.transpose(1,0))
adj0 = F.relu(torch.tanh(self.alpha*a))
adj_set.append(adj0)
return adj_set
class graph_constructor_one(nn.Module):
def __init__(self, nnodes, k, dim, layer_num, device, alpha=3, static_feat=None):
super(graph_constructor_one, self).__init__()
self.nnodes = nnodes
self.layers = layer_num
self.emb1 = nn.Embedding(nnodes, dim)
self.emb2 = nn.Embedding(nnodes, dim)
self.lin1 = nn.ModuleList()
self.lin2 = nn.ModuleList()
self.lin1 = nn.Linear(dim,dim)
self.lin2 = nn.Linear(dim,dim)
self.device = device
self.k = k
self.dim = dim
self.alpha = alpha
self.static_feat = static_feat
def forward(self, idx, scale_idx, scale_set):
nodevec1 = self.emb1(idx)
nodevec2 = self.emb2(idx)
adj_set = []
nodevec1 = torch.tanh(self.alpha*self.lin1(nodevec1))
nodevec2 = torch.tanh(self.alpha*self.lin2(nodevec2))
a = torch.mm(nodevec1, nodevec2.transpose(1,0))-torch.mm(nodevec2, nodevec1.transpose(1,0))
adj0 = F.relu(torch.tanh(self.alpha*a))
mask = torch.zeros(idx.size(0), idx.size(0)).to(self.device)
mask.fill_(float('0'))
s1,t1 = adj0.topk(self.k,1)
mask.scatter_(1,t1,s1.fill_(1))
adj = adj0*mask
return adj
GNN
在使用图的时候可以简单的将其划分为下面几步:
1.给定一个图,首先将节点转化为递归单元,将边转化为前馈神经网络;
2.对所有节点执行n次邻域聚合(消息传递)。
3.对所有节点的嵌入向量求和得到图表示H。
4.将H传递到更高的层中,或者使用它来表示图形的独特属性。
为什么要用GNN?
GNN与CNN、RNN的比较,为什么不用CNN或RNN,而用GNN?
GNN 是一种专门为图结构数据设计的神经网络。它可以处理不规则的拓扑结构,并且可以捕捉节点之间的复杂关系。GNN可以通过消息传递和聚合机制来处理不同大小和结构的图数据。由于其适应性和灵活性,GNN 在许多领域,如社交网络分析、化学分子识别和推荐系统等,取得了显著的成功。
选择 GNN 而不是 CNN 或 RNN 的主要原因是数据结构和任务类型。当处理图结构数据或需要捕捉复杂的节点关系时,GNNs是更合适的选择。然而,在处理网格结构数据(如图像)或序列数据(如文本)时,CNN和 RNN可能是更好的选择。
GNN简单来说就是Graph + Nerual Networks,关键问题就是将图的结构和图中每个节点和边的特征转化为一般的神经网络的输入(张量)。
GNN面临挑战
目前,GNN面临的挑战主要包括:1)如何有效处理大规模图数据;2)如何增强GNN的表达能力,以捕捉更为复杂的图结构;3)如何解决图数据中的不确定性和噪声问题。
总结
下周将继续学习GNN的一些相关数学知识,从理论上进一步理解GNN。