图神经网络实战(25)——基于A3T-GCN预测交通流量
0. 前言
近年来,智慧城市的概念日益流行。这一概念指的是利用数据来管理和改善运营与服务的城市。在这一背景下,智能交通系统的建立是其重要方面,准确的交通预测可以帮助交通管理者优化交通信号、规划基础设施并减少拥堵。然而,由于复杂的空间和时间依赖关系,交通预测是一个具有挑战性的问题。
在本节中,我们将把时空图神经网络 (Temporal Graph Neural Networks, TGNN) 应用于交通预测中。首先,我们将探索和处理 PeMS-M
数据集,从原始 CSV
文件创建一个动态图。然后,应用 TGNN
预测未来的交通流量。最后,对结果进行可视化并与基准模型进行比较,以验证架构的有效性。
1. PeMS-M 数据集分析
在本节中,我们将介绍 PeMS-M
数据集,以获取数据集相关信息。
我们在本节中所使用的数据集是 PeMSD7
数据集的变体。原始数据集是在 2012
年 5
月和 6
月的工作日使用加利福尼亚州性能测量系统 (Performance Measurement System
, PeMS
) 从 39,000
个传感器站点收集交通速度后获得的。在本节中,我们只考虑加利福尼亚州第 7
区的 228
个站点。这些站点输出 30
秒的速度测量值,在本数据集中以 5
分钟间隔进行汇总。
首先,从 gitcode 链接下载数据集并解压缩。
解压后的文件夹包含两个文件:V_228.csv
和 W_228.csv
。V_228.csv
文件包含 228
个传感器站点收集到的交通速度,而 W_228.csv
则存储了这些站点之间的距离。
(1) 使用 pandas
加载数据集,使用 range()
重命名列,以便于访问:
import pandas as pd
ratings = pd.read_csv('BX-Book-Ratings.csv', sep=';', encoding='latin-1')
users = pd.read_csv('BX-Users.csv', sep=';', encoding='latin-1')
books = pd.read_csv('BX-Books.csv', sep=';', encoding='latin-1', error_bad_lines=False)
首先可视化数据集中交通速度的变化,这是时间序列预测中的一个经典方法。另一方面,不稳定的时间序列可能需要进一步处理后才能使用。使用 matplotlib
绘制交通速度随时间变化的曲线。
(2) 导入 NumPy
和 matplotlib
:
import numpy as np
import matplotlib.pyplot as plt
(3) 使用 plt.plot()
为 DataFrame
中的每一行数据创建一个折线图:
plt.plot(speeds)
plt.grid(linestyle=':')
plt.xlabel('Time (5 min)')
plt.ylabel('Traffic speed')
plt.show()
可以看到,由于数据过于嘈杂,我们无法通过这种方法获得任何启示。相反,我们可以绘制几个传感器站点相应的数据,然而,这可能无法代表整个数据集。另一种选择是绘制平均交通速度和标准差。这样,我们就可以直观地看到数据集的概括信息。在实践中,我们会同时使用这两种方法,接下来,我们实现第二种方法。
(4) 计算每列对应的平均交通速度和相应的标准差:
mean = speeds.mean(axis=1)
std = speeds.std(axis=1)
(5) 用黑色实线绘制平均值:
plt.plot(mean, 'k-')
(6) 用 plt.fill_between()
将标准偏差绘制为浅红色:
plt.grid(linestyle=':')
plt.fill_between(mean.index, mean-std, mean+std, color='r', alpha=0.1)
plt.xlabel('Time (5 min)')
plt.ylabel('Traffic speed')
plt.show()
可以看到时间序列数据中明显的季节性(模式),除了第 5800
个数据样本附近。交通速度的变化很大,且包含明显峰值,因为传感器站点遍布加利福尼亚州第 7
区,一些传感器可能会遇到交通堵塞,但对于其他传感器可能没有。
我们可以通过绘制每个传感器的速度值之间的相关性来验证这一点。此外,我们还可以将其与每个站点之间的距离进行比较。相距较近的站点应该比相距较远的站点更常显示相似的数值。接下来,在同一图像中绘制以上两张图。
(7) 创建一个有两个水平子图的图像:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 8))
fig.tight_layout(pad=3.0)
(8) 使用 matshow()
函数绘制距离矩阵:
ax1.matshow(distances)
ax1.set_xlabel("Sensor station")
ax1.set_ylabel("Sensor station")
ax1.title.set_text("Distance matrix")
(9) 计算每个传感器站的 Pearson
相关系数,为此,我们必须对速度矩阵进行转置:
ax2.matshow(-np.corrcoef(speeds.T))
ax2.set_xlabel("Sensor station")
ax2.set_ylabel("Sensor station")
ax2.title.set_text("Correlation matrix")
plt.show()
可以看到,站点之间距离远并不意味着它们之间的相关性不高(反之亦然)。如果我们只考虑该数据集的一个子集,这一点尤为重要,距离较近的站点可能会有截然不同的输出,从而增加交通预测的难度。在本节中,我们将考虑数据集中的所有传感器站点。
2. 数据集预处理
掌握了有关该数据集的相关信息后,接下来,需要在将其输入时空图神经网络 (Temporal Graph Neural Networks, TGNN) 之前对其进行预处理。
第一步是将表格数据集转换为动态图。因此,我们首先需要根据原始数据创建一个图。换句话说,我们必须以有意义的方式连接不同的传感器站点。在本节中,我们通过使用距离矩阵连接各站点。
有多种方法可以根据距离矩阵计算邻接矩阵。例如,当两个站点之间的距离小于平均距离时,我们可以分配一条连接。在本节中,我们将采用一种更高级的处理方法来计算加权邻接矩阵。我们不再使用二进制值,而是使用以下公式计算介于 0
(无连接)和 1
(强连接)之间的权重:
w
i
j
=
{
e
x
p
(
−
d
i
j
2
σ
2
)
,
i
≠
j
a
n
d
e
x
p
(
−
d
i
j
2
σ
2
)
≥
ϵ
0
,
o
t
h
e
r
w
i
s
e
w_{ij}=\left\{ \begin{aligned} {exp(-\frac {d_{ij}^2}{\sigma^2})} &,& {i\neq j \ and \ exp(-\frac {d_{ij}^2}{\sigma^2})\geq \epsilon }\\ 0&,& {otherwise} \end{aligned} \right.
wij=⎩
⎨
⎧exp(−σ2dij2)0,,i=j and exp(−σ2dij2)≥ϵotherwise
其中,
w
i
j
w_{ij}
wij 代表从节点
i
i
i 到节点
j
j
j 的边的权重,
d
i
j
d_{ij}
dij 是这两个节点之间的距离,
σ
2
\sigma ^2
σ2 和
ϵ
\epsilon
ϵ 是两个阈值,用于控制邻接矩阵的分布和稀疏性,将阈值设定为
σ
2
=
0.1
\sigma ^2=0.1
σ2=0.1、
ϵ
=
0.5
\epsilon=0.5
ϵ=0.5。
接下来,使用 Python
进行实现,并绘制得到的邻接矩阵图。
(1) 创建一个计算邻接矩阵的函数,函数包含三个参数:距离矩阵、阈值
σ
2
\sigma^2
σ2 和
ϵ
\epsilon
ϵ,将距离除以 10,000
进行缩放并计算
d
2
d^2
d2:
def compute_adj(distances, sigma2=0.1, epsilon=0.5):
d = distances.to_numpy() / 10000.
d2 = d * d
当权重值大于或等于
ϵ
\epsilon
ϵ 时获得权重(否则,权重应等于零)。当我们测试权重是否大于或等于
ϵ
\epsilon
ϵ 时,返回结果为 True
或 False
,因此我们需要一个全为 1
的掩码 (w_mask
) 将其转换回 0
和 1
的值。再次对其进行乘法运算,得到权重大于或等于
ϵ
\epsilon
ϵ 的实数值:
n = distances.shape[0]
w_mask = np.ones([n, n]) - np.identity(n)
return np.exp(-d2 / sigma2) * (np.exp(-d2 / sigma2) >= epsilon) * w_mask
(2) 计算邻接矩阵并检查结果:
adj = compute_adj(distances)
print(adj[0])
'''
array([0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0.61266012, 0. , ...
'''
上述结果代表从节点 1
到节点 2
的边的权重。
(3) 更有效的可视化方法是使用 matplotlib
的 matshow
:
cax = plt.matshow(adj, False)
plt.colorbar(cax)
plt.xlabel("Sensor station")
plt.ylabel("Sensor station")
plt.show()
我们可以将它与之前绘制的距离矩阵进行比较,以观察相似之处。
(4) 还可以使用 networkx
直接将其绘制成图。在这种情况下,连接是二进制的,因此我们可以简单地认为每个权重都大于 0
。也可以使用边标签来显示这些值,但图的显示效果将过于杂乱:
import matplotlib.pyplot as plt
import networkx as nx
def plot_graph(adj):
plt.figure(figsize=(10,5))
rows, cols = np.where(adj > 0)
edges = zip(rows.tolist(), cols.tolist())
G = nx.Graph()
G.add_edges_from(edges)
nx.draw(G, with_labels=True)
plt.show()
即使没有标签,生成的图也并不便于阅读:
plot_graph(adj)
事实上,许多节点都是相互连接的,因为它们彼此非常接近。尽管如此,我们还是可以分辨出几条可能与实际道路相对应的分支。
获取图数据后,就可以专注于研究交通预测问题中的时间序列方面。第一步是对速度值进行归一化处理,以便将其输入神经网络。在交通预测中,许多模型都选择了 z-score
归一化(或标准化),接下来,我们将实现这一方法。
(5) 创建函数计算 z-score
:
def zscore(x, mean, std):
return (x - mean) / std
(6) 将其应用于数据集,创建归一化数据集:
speeds_norm = zscore(speeds, speeds.mean(axis=0), speeds.std(axis=0))
数据正确标准化后,我们可以用它们为每个节点创建时间序列。我们需要在每个时间步长
t
t
t 处获得
h
h
h 个输入数据样本,以预测
t
+
h
t + h
t+h 时的速度值,大量的输入数据样本也会增加数据集的内存占用。
h
h
h 的值取决于我们要执行的任务:短期还是长期交通预测。在本节中,我们将
h
h
h 值设置为 48
来预测 4
小时后的交通速度。
(7) 初始化变量:滞后数(输入数据样本数)、h
值、输入矩阵和真实值矩阵:
lags = 24
horizon = 48
xs = []
ys = []
(8) 对于每个时间步长
t
t
t,在 xs
中存储 12
个(滞后)先前值,在 ys
中存储
t
+
h
t+h
t+h 时的值:
for i in range(lags, speeds_norm.shape[0]-horizon):
xs.append(speeds_norm.to_numpy()[i-lags:i].T)
ys.append(speeds_norm.to_numpy()[i+horizon-1])
(9) 最后,使用 PyTorch Geometric Temporal
创建动态图。以 COO
格式创建边索引和加权邻接矩阵中的边权重:
edge_index = (np.array(adj) > 0).nonzero()
print(edge_index)
from torch_geometric_temporal.signal import StaticGraphTemporalSignal
dataset = StaticGraphTemporalSignal(edge_index, adj[adj > 0], xs, ys)
# (array([ 0, 0, 0, ..., 227, 227, 227]), array([ 7, 123, 129, ..., 221, 222, 224]))
(10) 打印第一个图的信息,观察结果:
print(dataset[0])
# Data(x=[228, 24], edge_index=[2, 1664], edge_attr=[1664], y=[228])
(11) 将训练/测试集进行拆分:
from torch_geometric_temporal.signal import temporal_signal_split
train_dataset, test_dataset = temporal_signal_split(dataset, train_ratio=0.8)
最终的动态图有 228
个节点,12
个值,1,664
个连接。接下来,我们应用 TGNN
预测交通流量。
3. 实现 A3T-GCN 架构
在本节中,我们将构建专为交通流量预测设计的注意力时空图卷积网络 (Attention Temporal Graph Convolutional Network
, A3T-GCN
)。这种架构能够考虑复杂的空间和时间依赖关系:
- 空间依赖性是指一个地点的交通状况会受到附近地点交通状况的影响。例如,交通堵塞往往会蔓延到邻近的道路
- 时间依赖性是指某一地点在某一时刻的交通状况会受到同一地点在之前时刻的交通状况的影响。例如,如果一条道路在早高峰期间拥堵,那么它很可能会一直拥堵到晚高峰
A3T-GCN
是对时空图卷积网络 (Temporal Graph Convolutional Network
, TGCN
) 架构的改进。TGCN
是图卷积网络 (Graph Convolutional Network, GCN) 和门控循环单元 (Gate Recurrent Unit
, GRU
) 的组合,根据每个输入时间序列产生隐藏向量,使用这种组合可以从输入中捕捉空间和时间信息。然后使用注意力模型计算权重并输出上下文向量,最终的预测结果基于生成的上下文向量。之所以要添加注意力模型,是因为需要了解全局趋势。接下来,使用 PyTorch Geometric Temporal
库来实现 A3T-GCN
。
(1) 导入所需的库:
import torch
from torch_geometric_temporal.nn.recurrent import A3TGCN
(2) 创建一个具有 A3TGCN
层和 32
个隐藏维度的线性层的 TGNN
。edge_attr
参数用于存储边权重:
class TemporalGNN(torch.nn.Module):
def __init__(self, dim_in, periods):
super().__init__()
self.tgnn = A3TGCN(in_channels=dim_in, out_channels=32, periods=periods)
self.linear = torch.nn.Linear(32, periods)
def forward(self, x, edge_index, edge_attr):
h = self.tgnn(x, edge_index, edge_attr).relu()
h = self.linear(h)
return h
(3) 实例化 TGNN
和学习率为 0.005
的 Adam
优化器。使用 CPU
训练模型:
model = TemporalGNN(lags, 1).to('cpu')
optimizer = torch.optim.Adam(model.parameters(), lr=0.005)
(4) 使用均方误差 (Mean Squared Error
, MSE
) 作为损失函数,对模型进行 30
个 epoch
训练:
model.train()
print(model)
# Training
for epoch in range(30):
loss = 0
step = 0
for i, snapshot in enumerate(train_dataset):
y_pred = model(snapshot.x.unsqueeze(2), snapshot.edge_index, snapshot.edge_attr)
loss += torch.mean((y_pred-snapshot.y)**2)
step += 1
loss = loss / (step + 1)
loss.backward()
optimizer.step()
optimizer.zero_grad()
if epoch % 5 == 0:
print(f"Epoch {epoch:>2} | Train MSE: {loss:.4f}")
模型训练完成后,对其进行评估。除了均方根误差 (Root Mean Squared ErrorR
, MSE
) 和平均绝对误差 (Mean Absolute Error
, MAE
) 等经典指标外,也可以用时间序列数据将模型与基线模型进行比较,接下来,我们将使用以下方法进行比较:
- 随机游走 (
Random Walk
,RW
):在这种情况下,RW
指的是使用最后一次观测值作为预测值。换句话说, t t t 时的值与 t + h t+h t+h 时的值相同 - 历史平均值 (
Historical Average
,HA
):在这种情况下,计算前 k k k 个样本的平均交通速度作为 t + h t+h t+h 时的值。在本节中,使用滞后期数作为 k k k 值,但也可以使用整体历史平均值
接下来,评估模型在测试集上的预测结果。
(1) 创建函数 inverse_zscore
来反转 z-score
,并返回原始值:
def inverse_zscore(x, mean, std):
return x * std + mean
(2) 调用函数 inverse_zscore
根据归一化值重新计算我们想要预测的速度:
y_test = []
for snapshot in test_dataset:
y_hat = snapshot.y.numpy()
y_hat = inverse_zscore(y_hat, speeds.mean(axis=0), speeds.std(axis=0))
y_test = np.append(y_test, y_hat)
(3) 对 GNN
所做的预测采用相同的策略:
gnn_pred = []
model.eval()
for snapshot in test_dataset:
snapshot = snapshot
y_hat = model(snapshot.x.unsqueeze(2), snapshot.edge_index, snapshot.edge_weight).squeeze().detach().numpy()
y_hat = inverse_zscore(y_hat, speeds.mean(axis=0), speeds.std(axis=0))
gnn_pred = np.append(gnn_pred, y_hat)
(4) 对 RW
和 HA
方法进行相同的处理:
rw_pred = []
for snapshot in test_dataset:
y_hat = snapshot.x[:,-1].squeeze().detach().numpy()
y_hat = inverse_zscore(y_hat, speeds.mean(axis=0), speeds.std(axis=0))
rw_pred = np.append(rw_pred, y_hat)
ha_pred = []
for i in range(lags, speeds_norm.shape[0]-horizon):
y_hat = speeds_norm.to_numpy()[:i].T.mean(axis=1)
y_hat = inverse_zscore(y_hat, speeds.mean(axis=0), speeds.std(axis=0))
ha_pred.append(y_hat)
ha_pred = np.array(ha_pred).flatten()[-len(y_test):]
(5) 创建函数来计算 MAE
、RMSE
和平均绝对误差 (Mean Absolute Percentage ErrorM
, APE
):
def MAE(real, pred):
return np.mean(np.abs(pred - real))
def RMSE(real, pred):
return np.sqrt(np.mean((pred - real) ** 2))
def MAPE(real, pred):
return np.mean(np.abs(pred - real) / (real + 1e-5))
(6) 评估 GNN 的预测结果,并对每种算法重复这一过程:
print(f'GNN MAE = {MAE(gnn_pred, y_test):.4f}')
print(f'GNN RMSE = {RMSE(gnn_pred, y_test):.4f}')
print(f'GNN MAPE = {MAPE(gnn_pred, y_test):.4f}')
print(f'RW MAE = {MAE(rw_pred, y_test):.4f}')
print(f'RW RMSE = {RMSE(rw_pred, y_test):.4f}')
print(f'RW MAPE = {MAPE(rw_pred, y_test):.4f}')
print(f'HA MAE = {MAE(ha_pred, y_test):.4f}')
print(f'HA RMSE = {RMSE(ha_pred, y_test):.4f}')
print(f'HA MAPE = {MAPE(ha_pred, y_test):.4f}')
结果如下表所示:
RMSE | MAE | MAPE | |
---|---|---|---|
A3T-GCN | 12.0901 | 8.4123 | 0.1503 |
Random Walk | 17.6501 | 11.0469 | 0.2999 |
Historical Average | 13.1518 | 9.3374 | 0.1633 |
可以看到,在每个指标上,A3T-GCN
模型都优于基准技术。我们也可以将这些指标与长短期记忆 (Long Short-Term Memory, LSTM) 或 GRU
网络提供的预测进行比较,以衡量拓扑信息的重要性。
最后,绘制平均预测值,以进行可视化。
(1) 使用列表推导式来获取平均预测值:
y_preds = [inverse_zscore(model(snapshot.x.unsqueeze(2), snapshot.edge_index, snapshot.edge_weight).squeeze().detach().numpy(), speeds.mean(axis=0), speeds.std(axis=0)).mean() for snapshot in test_dataset]
(2) 计算原始数据集的平均值和标准差:
mean = speeds.mean(axis=1)
std = speeds.std(axis=1)
(3) 绘制平均交通速度与标准差,并将其与预测值( t + 4 t + 4 t+4 小时)进行比较:
plt.plot(np.array(mean), 'k-', label='Mean')
plt.plot(range(len(speeds)-len(y_preds), len(speeds)), y_preds, 'r-', label='Prediction')
plt.grid(linestyle=':')
plt.fill_between(mean.index, mean-std, mean+std, color='r', alpha=0.1)
plt.axvline(x=len(speeds)-len(y_preds), color='b', linestyle='--')
plt.xlabel('Time (5 min)')
plt.ylabel('Traffic speed to predict')
plt.legend(loc='upper right')
plt.show()
T-GNN
可以正确预测峰值,并遵循全局趋势。然而,由于 MSE
损失使得模型犯严重错误代价更高,因此预测的速度更接近于整体平均值。尽管如此,GNN
仍然相当精确。
小结
本节重点讨论了使用 TGNN
进行交通预测任务。首先,我们介绍了 PeMS-M
数据集,并将其从表格数据转换为具有时间信号的静态图数据集。在实践中,我们根据输入距离矩阵创建了加权邻接矩阵,并将交通速度转换为时间序列。最后,我们构建了 A3T-GCN
模型,这是一个专为交通预测设计的 T-GNN
模型,并将结果与两个基线模型进行了比较,验证了模型的预测结果。
系列链接
图神经网络实战(1)——图神经网络(Graph Neural Networks, GNN)基础
图神经网络实战(2)——图论基础
图神经网络实战(3)——基于DeepWalk创建节点表示
图神经网络实战(4)——基于Node2Vec改进嵌入质量
图神经网络实战(5)——常用图数据集
图神经网络实战(6)——使用PyTorch构建图神经网络
图神经网络实战(7)——图卷积网络(Graph Convolutional Network, GCN)详解与实现
图神经网络实战(8)——图注意力网络(Graph Attention Networks, GAT)
图神经网络实战(9)——GraphSAGE详解与实现
图神经网络实战(10)——归纳学习
图神经网络实战(11)——Weisfeiler-Leman测试
图神经网络实战(12)——图同构网络(Graph Isomorphism Network, GIN)
图神经网络实战(13)——经典链接预测算法
图神经网络实战(14)——基于节点嵌入预测链接
图神经网络实战(15)——SEAL链接预测算法
图神经网络实战(16)——经典图生成算法
图神经网络实战(17)——深度图生成模型
图神经网络实战(18)——消息传播神经网络
图神经网络实战(19)——异构图神经网络
图神经网络实战(20)——时空图神经网络
图神经网络实战(21)——图神经网络的可解释性
图神经网络实战(22)——基于Captum解释图神经网络
图神经网络实战(23)——使用异构图神经网络执行异常检测
图神经网络实战(24)——基于LightGCN构建推荐系统
图神经网络实战——总结 | 图神经网络展望