☰
STGCN交通流预测实战:从IJCAI-18论文到Python工程复现
2026/10/12 20:04:00 网站建设 项目流程

简介:这份资源是IJCAI 2018论文STGCN(时空图卷积网络)的官方源码复现包,面向交通工程、智能交通系统与深度学习方向的研究者和开发者,用于解决城市交通流量预测这一典型时空序列建模问题。模型通过图卷积捕捉监测点之间的空间依赖,结合时间卷积刻画流量演变规律,是学习非欧几里得结构数据建模的优质案例。压缩包共19个文件,以11个Python脚本为主,涵盖模型定义、训练与测试流程、数据加载与图结构工具,另含6张PNG结果图、1份README说明及1个数据压缩包,整体约7.05MB。目录中models、data_loader、utils等模块划分清晰,便于按需阅读与二次开发。已有2309人学习下载,读者可借此掌握交通流数据的预处理、STGCN训练调参、预测结果可视化与性能评估的完整链路,适合作为课程设计、论文复现或智能交通项目入门的参考。

1. STGCN 交通流预测:从 IJCAI-18 论文到 Python 可跑通的工程路径

城市路网的交通流预测,本质上是在一张非欧几里得的图结构上做时序回归。2018 年 IJCAI 收录的 STGCN(Spatio-Temporal Graph Convolutional Networks)之所以被反复提起,是因为它第一次把图卷积和门控一维卷积拼成一套完整的时空块,在 PeMSD 这类公开数据集上把预测误差压到了当时的领先水平。很多人拿到STGCN_IJCAI-18-master这个 Python 工程后卡在第一步:环境装不上、数据对不齐、邻接矩阵不知道怎么构建。这篇笔记按「先立住原理、再动手复现、最后讲坑」的顺序,把这条路径拆成能照着做的步骤,适合已经会写 Python、想把这套模型真正跑起来做交通流预测的从业者。

2. STGCN 到底在算什么:图卷积加门控时序卷积的组合逻辑

2.1 交通流预测为什么不能直接用 LSTM 或 CNN

先把问题定义清楚。给定路网上 N 个检测器(或路段)在过去 M 个时间片的流量序列,预测未来 H 个时间片的流量。输入是一个形状为 (M, N, C) 的张量,C 是特征通道数(通常就是流量这一个通道,也可以叠加占有率、速度)。

普通 LSTM 把每个检测器当成独立序列处理,丢掉了路段之间的空间依赖;普通 CNN 把路网当成规则网格,但城市路网是拓扑图,相邻关系不是上下左右。STGCN 的核心主张就是:空间维度用图卷积在拓扑图上聚合邻居信息,时间维度用一维卷积沿时间轴滑动,两者交替堆叠。

这里有个容易被忽略的点:STGCN 用的是谱域图卷积的切比雪夫一阶近似,也就是常说的 GCN 那一套。它不需要真的做特征分解,而是用邻接矩阵和度矩阵构造一个归一化的拉普拉斯算子,直接做矩阵乘法。这就是为什么工程里最关键的输入之一是你自己构建的邻接矩阵。

2.2 STGCN 的网络结构拆解

一个标准 STGCN 块(ST-Conv Block)由三部分组成,顺序是「时间卷积 → 空间图卷积 → 时间卷积」,中间夹着层归一化和残差连接。时间卷积用的是门控线性单元(GLU),一维卷积输出被切成两半,一半过 sigmoid 当门控,另一半直接乘上去。这个设计比单纯 ReLU 更能控制信息流。

堆叠两个 ST-Conv Block 之后,接一个输出层(也是时间卷积),把通道数映射到预测步长 H。整个前向过程可以概括成:

  • 输入 (M, N, C) → 第一层时间卷积把 C 升到 64
  • 空间图卷积在 N 个节点间聚合
  • 第二层时间卷积把时间维从 M 压到 M-K+1
  • 重复一次,最后输出 (H, N, 1)

参数上,通道数 64、卷积核时间跨度 Kt=3、图卷积切比雪夫阶数 Ks=3 是论文里的默认配置。这几个数字后面调参时会反复用到。

2.3 邻接矩阵怎么构建:距离阈值还是相关性

这是整个工程里最影响效果、也最容易被糊弄的一步。STGCN 原文用的是基于路网距离的高斯核:

W_ij = exp(-(dist(i,j)^2) / sigma^2) 当 dist(i,j) <= kappa W_ij = 0 当 dist(i,j) > kappa

kappa是阈值,超过就认为两个节点不连通;sigma控制衰减速度。PeMSD 数据集里检测器的实际距离是给定的,所以能这么算。但如果你用的是自己公司的数据,没有路段距离,常见做法有两种:一是用历史流量序列的皮尔逊相关系数构建相关性邻接矩阵,二是用路网拓扑的连通性构建 0/1 邻接矩阵再做归一化。

我一般会先跑一版距离邻接矩阵作为 baseline,再试相关性矩阵对比。相关性矩阵在数据充足时往往更好,因为它捕捉的是「流量变化同步」而不是「物理靠近」,但要注意相关系数阈值设太低会引入噪声边,设太高图会变得稀疏、图卷积退化成近似孤立节点。

3. 把 STGCN_IJCAI-18-master 在本地跑起来的最小步骤

3.1 环境准备与依赖安装

这个工程是纯 Python + PyTorch 实现,不依赖 PyTorch Geometric 之类的重型图神经网络库,图卷积是自己用torch.matmul写的。所以环境相对干净。推荐 Python 3.8 到 3.10,PyTorch 1.10 以上。

# 创建独立环境,避免和系统里的其他包冲突 conda create -n stgcn python=3.8 -y conda activate stgcn # 安装 PyTorch,按自己机器的 CUDA 版本选,没有 GPU 就用 CPU 版 pip install torch==1.13.1 torchvision==0.14.1 # 工程常用依赖 pip install numpy pandas scipy scikit-learn matplotlib tensorboardX

逻辑说明:torch是核心,numpy/scipy用来处理邻接矩阵和距离计算,pandas读 PeMSD 的 csv,tensorboardX是原工程用来记录 loss 的。参数上,如果你机器没有 NVIDIA 显卡,PyTorch 装 CPU 版即可,训练会慢但小数据集能跑通。注意不要盲目装最新版 PyTorch,老工程里有些 API(比如torch.nn.functional的某些签名)在新版本会有 warning 甚至报错。

3.2 数据准备与目录结构对齐

PeMSD 数据集(PeMSD4、PeMSD7、PeMSD8)是公开的,工程里通常有一个data/目录放原始文件。你需要确认三样东西到位:流量数据文件(.npz 或 .csv)、邻接矩阵文件、以及数据划分的索引。

import numpy as np # 读取流量数据,形状一般是 (总时间片, 节点数, 特征数) data = np.load('data/PeMSD7/pems07.npz')['data'] print('原始数据形状:', data.shape) # 例如 (12672, 228, 3) # 只取流量这一列,或者按需取多通道 flow = data[:, :, 0:1] print('流量张量形状:', flow.shape) # 按 6:2:2 划分训练/验证/测试 total = flow.shape[0] train_end = int(total * 0.6) val_end = int(total * 0.8) train = flow[:train_end] val = flow[train_end:val_end] test = flow[val_end:] print(train.shape, val.shape, test.shape)

逻辑说明:STGCN 的输入需要按时间窗口切片,工程里通常用Dataset类做滑动窗口。上面这段只是确认数据形状和划分比例。参数上,PeMSD7 是 228 个节点、每 5 分钟一个时间片,一天 288 个片。划分比例 6:2:2 是论文常用设置,你也可以按 7:1:2 调,但验证集太小会导致早停不稳定。

3.3 邻接矩阵的构建代码

如果工程自带了邻接矩阵文件,直接用;如果没有,或者你想换成相关性矩阵,就自己算。

import numpy as np from scipy.spatial.distance import cdist def build_distance_adj(distance_matrix, kappa=0.1, sigma=1.0): """ 基于距离构建高斯核邻接矩阵 distance_matrix: (N, N) 节点间距离 kappa: 距离阈值,超过则权重置 0 sigma: 高斯核带宽 """ N = distance_matrix.shape[0] adj = np.zeros((N, N)) for i in range(N): for j in range(N): if distance_matrix[i, j] <= kappa: adj[i, j] = np.exp(-(distance_matrix[i, j] ** 2) / (sigma ** 2)) return adj def normalize_adj(adj): """对称归一化 D^-1/2 (A+I) D^-1/2""" adj = adj + np.eye(adj.shape[0]) # 加自环 d = np.sum(adj, axis=1) d_inv_sqrt = np.power(d, -0.5) d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0 D_inv_sqrt = np.diag(d_inv_sqrt) return D_inv_sqrt @ adj @ D_inv_sqrt # 假设 distance_matrix 已经算好 adj = build_distance_adj(distance_matrix, kappa=0.1, sigma=1.0) adj_norm = normalize_adj(adj) np.save('data/adj_norm.npy', adj_norm)

逻辑说明:build_distance_adj里那个双重循环在 N=228 时能跑,但 N 上千就会很慢,实际工程里应该用向量化写法。normalize_adj做的是 GCN 标准的对称归一化,加自环是为了让节点在聚合时保留自身信息。参数上,kappa和sigma需要根据你的距离量纲调,如果距离单位是公里,kappa设 0.1 可能太小导致图太稀疏,设 1.0 更常见。这一步做完,把adj_norm.npy喂给模型即可。

3.4 训练脚本的关键参数

工程里一般有main.py或train.py。启动命令大致是:

python main.py --device cuda:0 --data data/PeMSD7 --adj data/adj_norm.npy \ --batch_size 50 --epochs 50 --lr 0.001 --Kt 3 --Ks 3 --channel 64

参数说明:batch_size50 是论文设置,显存不够就降到 32 或 16;lr用 0.001 配 Adam,训练不稳定时降到 0.0005;Kt是时间卷积核大小,Ks是图卷积切比雪夫阶数,这两个一般不动;channel是隐藏层通道数,64 是默认,数据量大可以升到 128。训练过程中重点看验证集的 MAE 和 RMSE,如果训练 loss 一直降但验证 loss 反弹,就是过拟合,加 dropout 或减通道数。

4. 交通流预测里最容易翻车的几个地方

4.1 现象:loss 正常下降但预测曲线整体平移

原因:数据归一化用了全局均值方差,但训练集和测试集的流量分布不一致,导致模型学到的是训练集的偏移量。解决:改成按节点做 z-score 归一化,或者用训练集的统计量去归一化验证和测试集,绝不能用全量数据算均值方差。

4.2 现象:图卷积层输出全是 NaN

原因:邻接矩阵归一化时出现了除零,d_inv_sqrt里有 inf。解决:在normalize_adj里加np.isinf判断并置 0,或者给度矩阵加一个极小值1e-6。这个坑在节点度数为 0 时必现。

4.3 现象:训练速度极慢,GPU 利用率低

原因:数据加载用了 Python 原生循环逐样本处理,没有用DataLoader的多进程。解决:把滑动窗口切片放到Dataset.__getitem__里,DataLoader设num_workers=4,并把数据提前转成torch.Tensor放到 pin memory。

4.4 现象:换了自己的数据后 MAE 比论文差一大截

原因:邻接矩阵和实际路网不匹配,或者时间片粒度和论文不一致(论文是 5 分钟,你的数据可能是 1 分钟或 15 分钟)。解决:先确认时间粒度,重新按对应步长构造输入输出窗口;邻接矩阵先用拓扑连通性做一版,再逐步换相关性矩阵对比。

4.5 现象:多步预测时后几步误差爆炸

原因:直接多步输出(一次性输出 H 步)在 H 较大时误差累积。解决:改成滚动预测,每次只预测一步,把预测值拼回输入再预测下一步;或者用 seq2seq 结构加 teacher forcing。STGCN 原版是直接多步,H=12 时还能接受,H 再大就要换策略。

5. 让 STGCN 真正可用的两个进阶技巧

第一个技巧是残差连接的位置。原工程在 ST-Conv Block 里把残差加在第二个时间卷积之后,但如果你把残差改成加在块输入和块输出之间(类似 ResNet 的 identity mapping),在深层堆叠时梯度会更稳。我试过堆 4 个块,不加跨块残差时第三块开始梯度就很小了,加上之后 4 块还能正常收敛。具体做法是在forward里保存块输入x_in,最后return x + x_in,前提是通道数一致,不一致就用 1x1 卷积对齐。

第二个技巧是用验证集上的滚动预测做早停。很多人只监控单步 MAE,但交通流预测真正关心的是未来 15 分钟、30 分钟、60 分钟的整体误差。我一般会在每个 epoch 结束后,用验证集做一次完整的 12 步滚动预测,算平均 MAE,用这个指标做早停和模型选择。这样选出来的模型在实际部署时表现更稳,不会出现「单步很好、多步崩掉」的情况。

def rolling_predict(model, history, steps=12): """ history: (M, N, C) 历史窗口 逐步预测未来 steps 步 """ preds = [] cur = history.clone() for _ in range(steps): with torch.no_grad(): y = model(cur) # 输出 (1, N, 1) preds.append(y) # 把预测值拼到输入末尾,去掉最老的一步 cur = torch.cat([cur[:, :, 1:], y], dim=2) return torch.cat(preds, dim=0)

逻辑说明:这段滚动预测每次只信模型的一步输出,然后把它当成真实值继续喂。参数上steps对应你要预测的未来步数,history的时间窗口长度要和训练时一致。注意这里没有做梯度回传,所以推理阶段用torch.no_grad()包住,显存占用小很多。

最后说个我自己的习惯:每次换数据集或改邻接矩阵,我都会先跑一个只有 1 个 ST-Conv Block 的小模型,确认数据管道和 loss 能正常下降,再上完整模型。这样能把「数据问题」和「模型问题」分开,省掉大量瞎调参的时间。STGCN 这套东西不算新,但在交通流预测这个场景里,它的结构清晰、复现成本低,仍然是值得先跑通再谈改进的 baseline。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询